1#![allow(unused_imports)]
2
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use arc_swap::ArcSwap;
7
8use tower_lsp::jsonrpc::Result;
9use tower_lsp::lsp_types::notification::Progress as ProgressNotification;
10use tower_lsp::lsp_types::request::WorkDoneProgressCreate;
11use tower_lsp::lsp_types::*;
12use tower_lsp::{Client, LanguageServer, async_trait};
13
14use php_ast::{
15 ClassMember, ClassMemberKind, EnumMember, EnumMemberKind, ExprKind, NamespaceBody, Stmt,
16 StmtKind,
17};
18
19use crate::ast::{ParsedDoc, str_offset};
20use crate::autoload::Psr4Map;
21use crate::completion::{CompletionCtx, filtered_completions_at};
22use crate::config::LspConfig;
23use crate::document_store::DocumentStore;
24use crate::file_rename::{use_edits_for_delete, use_edits_for_rename};
25use crate::hover::{
26 class_hover_from_index, docs_for_symbol_from_index, hover_info_with_maps,
27 signature_for_symbol_from_index,
28};
29use crate::open_files::{OpenFiles, compute_open_file_diagnostics};
30use crate::panic_guard::{guard_async, guard_async_result};
31use crate::phpstorm_meta::PhpStormMeta;
32use crate::symbols::{
33 document_symbols, resolve_workspace_symbol, workspace_symbols_from_workspace,
34};
35use crate::use_import::{build_use_import_edit, find_fqn_for_class};
36use crate::util::{fqn_short_name, word_at_position};
37use crate::workspace_scan::{scan_workspace, send_refresh_requests};
38
39use crate::actions::extract_action::extract_variable_actions;
40use crate::actions::extract_constant_action::extract_constant_actions;
41use crate::actions::extract_method_action::extract_method_actions;
42use crate::actions::generate_action::{
43 generate_constructor_actions, generate_getters_setters_actions,
44};
45use crate::actions::implement_action::implement_missing_actions;
46use crate::actions::inline_action::inline_variable_actions;
47use crate::actions::phpdoc_action::phpdoc_actions;
48use crate::actions::promote_action::promote_constructor_actions;
49use crate::actions::type_action::add_return_type_actions;
50
51use crate::navigation::call_hierarchy::{
52 incoming_calls, outgoing_calls_indexed, prepare_call_hierarchy_indexed,
53};
54use crate::navigation::declaration::{goto_declaration, goto_declaration_from_index};
55use crate::navigation::definition::{
56 find_declaration_range, find_method_in_class_hierarchy, find_method_range_in_class,
57 goto_definition,
58};
59use crate::navigation::implementation::{
60 find_implementations, find_implementations_from_workspace,
61};
62use crate::navigation::moniker::moniker_at;
63use crate::navigation::references::{
64 SymbolKind, find_constructor_references, find_references, find_references_with_target,
65};
66use crate::navigation::type_definition::{goto_type_definition, goto_type_definition_from_index};
67use crate::navigation::type_hierarchy::{
68 prepare_type_hierarchy_from_workspace, subtypes_of_from_workspace, supertypes_of_from_workspace,
69};
70
71use crate::analysis::code_lens::code_lenses;
72use crate::analysis::diagnostics::{
73 merge_file_diagnostics, parse_document, parse_document_no_diags,
74};
75use crate::analysis::document_highlight::document_highlights;
76use crate::analysis::inlay_hints::inlay_hints;
77use crate::analysis::inline_value::inline_values_in_range;
78use crate::analysis::semantic_tokens::{
79 compute_token_delta, legend, semantic_tokens, semantic_tokens_range, token_hash,
80};
81
82use crate::editing::document_link::document_links;
83use crate::editing::folding::folding_ranges;
84use crate::editing::formatting::{format_document, format_range};
85use crate::editing::on_type_format::on_type_format;
86use crate::editing::organize_imports::organize_imports_action;
87use crate::editing::rename::{prepare_rename, rename, rename_property, rename_variable};
88use crate::editing::selection_range::selection_ranges;
89use crate::editing::signature_help::signature_help;
90
91use super::helpers::{
92 DEFERRED_ACTION_TAGS, class_name_at_construct_decl, cursor_is_on_constant_decl,
93 cursor_is_on_method_decl, cursor_is_on_property_decl, defer_actions, is_after_arrow,
94 php_file_op, position_to_byte_offset, promoted_property_at_cursor, range_within, run_phpunit,
95 symbol_kind_at,
96};
97use super::{
98 Backend, IndexReadyNotification, build_mir_symbol, compute_dependent_publishes_owned,
99 compute_diagnostic_result_id, resolve_reference_symbol,
100};
101
102#[async_trait]
103impl LanguageServer for Backend {
104 async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
105 {
108 let mut roots: Vec<PathBuf> = params
109 .workspace_folders
110 .as_deref()
111 .unwrap_or(&[])
112 .iter()
113 .filter_map(|f| f.uri.to_file_path().ok())
114 .collect();
115 if roots.is_empty()
116 && let Some(path) = params.root_uri.as_ref().and_then(|u| u.to_file_path().ok())
117 {
118 roots.push(path);
119 }
120 self.root_paths.store(Arc::new(roots));
121 }
122
123 {
124 let opts = params.initialization_options.as_ref();
125 let roots = self.root_paths.load_full();
126 let file_cfg = crate::autoload::load_project_config_json(&roots);
127
128 if matches!(file_cfg, Some(serde_json::Value::Null)) {
129 self.client
130 .log_message(
131 tower_lsp::lsp_types::MessageType::WARNING,
132 "php-lsp: .php-lsp.json contains invalid JSON — ignoring",
133 )
134 .await;
135 }
136
137 if let Some(serde_json::Value::Object(ref obj)) = file_cfg
138 && let Some(ver) = obj.get("phpVersion").and_then(|v| v.as_str())
139 && !crate::autoload::is_valid_php_version(ver)
140 {
141 self.client
142 .log_message(
143 tower_lsp::lsp_types::MessageType::WARNING,
144 format!(
145 "php-lsp: .php-lsp.json unsupported phpVersion {ver:?} — valid values: {}",
146 crate::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
147 ),
148 )
149 .await;
150 }
151
152 if let Some(ver) = opts
153 .and_then(|o| o.get("phpVersion"))
154 .and_then(|v| v.as_str())
155 && !crate::autoload::is_valid_php_version(ver)
156 {
157 self.client
158 .log_message(
159 tower_lsp::lsp_types::MessageType::WARNING,
160 format!(
161 "php-lsp: unsupported phpVersion {ver:?} — valid values: {}",
162 crate::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
163 ),
164 )
165 .await;
166 }
167
168 let file_obj = file_cfg.as_ref().filter(|v| v.is_object());
171 let merged = LspConfig::merge_project_configs(file_obj, opts);
172 let mut cfg = LspConfig::from_value(&merged);
173
174 let roots_for_psr4 = (*roots).clone();
179 let roots_for_ver = (*roots).clone();
180 let explicit_version = cfg.php_version.clone();
181 let (psr4_result, ver_result) = tokio::join!(
182 tokio::task::spawn_blocking(move || {
183 let mut merged = Psr4Map::empty();
184 for root in &roots_for_psr4 {
185 merged.extend(Psr4Map::load(root));
186 }
187 merged
188 }),
189 tokio::task::spawn_blocking(move || {
190 crate::autoload::resolve_php_version_from_roots(
191 &roots_for_ver,
192 explicit_version.as_deref(),
193 )
194 }),
195 );
196 if let Ok(psr4) = psr4_result {
197 self.psr4.store(Arc::new(psr4));
198 }
199 let (ver, source) =
200 ver_result.unwrap_or_else(|_| (crate::autoload::PHP_8_5.to_string(), "default"));
201 self.client
202 .log_message(
203 tower_lsp::lsp_types::MessageType::INFO,
204 format!("php-lsp: using PHP {ver} ({source})"),
205 )
206 .await;
207 let ver = if source != "set by editor" && !crate::autoload::is_valid_php_version(&ver) {
208 let clamped = crate::autoload::clamp_php_version(&ver);
209 self.client
210 .show_message(
211 tower_lsp::lsp_types::MessageType::WARNING,
212 format!(
213 "php-lsp: detected PHP {ver} is outside the supported range \
214 ({}); using PHP {clamped} for analysis",
215 crate::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
216 ),
217 )
218 .await;
219 clamped.to_string()
220 } else {
221 ver
222 };
223 cfg.php_version = Some(ver.clone());
224 if let Ok(pv) = ver.parse::<mir_analyzer::PhpVersion>() {
225 self.docs.set_php_version(pv);
226 }
227 self.config.store(Arc::new(cfg));
228 }
229
230 let feat = self.config.load().features.clone();
231 Ok(InitializeResult {
232 capabilities: ServerCapabilities {
233 text_document_sync: Some(TextDocumentSyncCapability::Options(
234 TextDocumentSyncOptions {
235 open_close: Some(true),
236 change: Some(TextDocumentSyncKind::INCREMENTAL),
237 will_save: Some(true),
238 will_save_wait_until: Some(true),
239 save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
240 include_text: Some(false),
241 })),
242 },
243 )),
244 completion_provider: feat.completion.then(|| CompletionOptions {
245 trigger_characters: Some(vec![
246 "$".to_string(),
247 ">".to_string(),
248 ":".to_string(),
249 "(".to_string(),
250 "[".to_string(),
251 ]),
252 resolve_provider: Some(true),
253 ..Default::default()
254 }),
255 hover_provider: feat.hover.then_some(HoverProviderCapability::Simple(true)),
256 definition_provider: feat.definition.then_some(OneOf::Left(true)),
257 references_provider: feat.references.then_some(OneOf::Left(true)),
258 document_symbol_provider: feat.document_symbols.then_some(OneOf::Left(true)),
259 workspace_symbol_provider: feat.workspace_symbols.then(|| {
260 OneOf::Right(WorkspaceSymbolOptions {
261 resolve_provider: Some(true),
262 work_done_progress_options: Default::default(),
263 })
264 }),
265 rename_provider: feat.rename.then(|| {
266 OneOf::Right(RenameOptions {
267 prepare_provider: Some(true),
268 work_done_progress_options: Default::default(),
269 })
270 }),
271 signature_help_provider: feat.signature_help.then(|| SignatureHelpOptions {
272 trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
273 retrigger_characters: None,
274 work_done_progress_options: Default::default(),
275 }),
276 inlay_hint_provider: feat.inlay_hints.then(|| {
277 OneOf::Right(InlayHintServerCapabilities::Options(InlayHintOptions {
278 resolve_provider: Some(true),
279 work_done_progress_options: Default::default(),
280 }))
281 }),
282 folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
283 semantic_tokens_provider: feat.semantic_tokens.then(|| {
284 SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions {
285 legend: legend(),
286 full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
287 range: Some(true),
288 ..Default::default()
289 })
290 }),
291 selection_range_provider: feat
292 .selection_range
293 .then_some(SelectionRangeProviderCapability::Simple(true)),
294 call_hierarchy_provider: feat
295 .call_hierarchy
296 .then_some(CallHierarchyServerCapability::Simple(true)),
297 document_highlight_provider: feat.document_highlight.then_some(OneOf::Left(true)),
298 implementation_provider: feat
299 .implementation
300 .then_some(ImplementationProviderCapability::Simple(true)),
301 code_action_provider: feat.code_action.then(|| {
302 CodeActionProviderCapability::Options(CodeActionOptions {
303 resolve_provider: Some(true),
304 ..Default::default()
305 })
306 }),
307 declaration_provider: feat
308 .declaration
309 .then_some(DeclarationCapability::Simple(true)),
310 type_definition_provider: feat
311 .type_definition
312 .then_some(TypeDefinitionProviderCapability::Simple(true)),
313 code_lens_provider: feat.code_lens.then_some(CodeLensOptions {
314 resolve_provider: Some(true),
315 }),
316 document_formatting_provider: feat.formatting.then_some(OneOf::Left(true)),
317 document_range_formatting_provider: feat
318 .range_formatting
319 .then_some(OneOf::Left(true)),
320 document_on_type_formatting_provider: feat.on_type_formatting.then(|| {
321 DocumentOnTypeFormattingOptions {
322 first_trigger_character: "}".to_string(),
323 more_trigger_character: Some(vec!["\n".to_string()]),
324 }
325 }),
326 document_link_provider: feat.document_link.then(|| DocumentLinkOptions {
327 resolve_provider: Some(true),
328 work_done_progress_options: Default::default(),
329 }),
330 execute_command_provider: Some(ExecuteCommandOptions {
331 commands: vec!["php-lsp.runTest".to_string()],
332 work_done_progress_options: Default::default(),
333 }),
334 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(
335 DiagnosticOptions {
336 identifier: None,
337 inter_file_dependencies: true,
338 workspace_diagnostics: true,
339 work_done_progress_options: Default::default(),
340 },
341 )),
342 workspace: Some(WorkspaceServerCapabilities {
343 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
344 supported: Some(true),
345 change_notifications: Some(OneOf::Left(true)),
346 }),
347 file_operations: Some(WorkspaceFileOperationsServerCapabilities {
348 will_rename: Some(php_file_op()),
349 did_rename: Some(php_file_op()),
350 will_create: Some(php_file_op()),
351 did_create: Some(php_file_op()),
352 will_delete: Some(php_file_op()),
353 did_delete: Some(php_file_op()),
354 }),
355 }),
356 linked_editing_range_provider: feat
357 .linked_editing_range
358 .then_some(LinkedEditingRangeServerCapabilities::Simple(true)),
359 moniker_provider: Some(OneOf::Left(true)),
360 inline_value_provider: feat.inline_values.then(|| {
361 OneOf::Right(InlineValueServerCapabilities::Options(InlineValueOptions {
362 work_done_progress_options: Default::default(),
363 }))
364 }),
365 ..Default::default()
366 },
367 ..Default::default()
368 })
369 }
370
371 async fn initialized(&self, _params: InitializedParams) {
372 let php_selector = serde_json::json!([{"language": "php"}]);
374 let registrations = vec![
375 Registration {
376 id: "php-lsp-file-watcher".to_string(),
377 method: "workspace/didChangeWatchedFiles".to_string(),
378 register_options: Some(serde_json::json!({
379 "watchers": [{"globPattern": "**/*.php"}]
380 })),
381 },
382 Registration {
385 id: "php-lsp-type-hierarchy".to_string(),
386 method: "textDocument/prepareTypeHierarchy".to_string(),
387 register_options: Some(serde_json::json!({"documentSelector": php_selector})),
388 },
389 Registration {
390 id: "php-lsp-config-change".to_string(),
391 method: "workspace/didChangeConfiguration".to_string(),
392 register_options: Some(serde_json::json!({"section": "php-lsp"})),
393 },
394 ];
395 self.client.register_capability(registrations).await.ok();
396
397 let roots: Vec<PathBuf> = (**self.root_paths.load()).clone();
398 if !roots.is_empty() {
399 {
400 let mut merged = Psr4Map::empty();
401 for root in &roots {
402 merged.extend(Psr4Map::load(root));
403 }
404 self.psr4.store(Arc::new(merged));
405 }
406 self.meta.store(Arc::new(PhpStormMeta::load(&roots[0])));
407
408 let token = NumberOrString::String("php-lsp/indexing".to_string());
409 self.client
410 .send_request::<WorkDoneProgressCreate>(WorkDoneProgressCreateParams {
411 token: token.clone(),
412 })
413 .await
414 .ok();
415
416 let warm_docs = Arc::clone(&self.docs);
420 tokio::task::spawn_blocking(move || {
421 let php_version = warm_docs.workspace_php_version();
422 warm_docs.analysis_session(php_version);
423 });
424
425 let docs = Arc::clone(&self.docs);
426 let open_files = self.open_files.clone();
427 let client = self.client.clone();
428 let (exclude_paths, include_paths, max_indexed_files) = {
429 let cfg = self.config.load();
430 let mut exclude = cfg.exclude_paths.clone();
431 if !cfg.index_vendor && !exclude.iter().any(|p| p == "vendor" || p == "vendor/") {
437 exclude.push("vendor/".to_string());
438 }
439 (exclude, cfg.include_paths.clone(), cfg.max_indexed_files)
440 };
441 tokio::spawn(async move {
442 client
443 .send_notification::<ProgressNotification>(ProgressParams {
444 token: token.clone(),
445 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(
446 WorkDoneProgressBegin {
447 title: "php-lsp: indexing workspace".to_string(),
448 cancellable: Some(false),
449 message: None,
450 percentage: None,
451 },
452 )),
453 })
454 .await;
455
456 let mut total = 0usize;
457 let mut session_cache_set = false;
458 for root in roots {
459 let cache = crate::cache::WorkspaceCache::new(&root);
465 if !session_cache_set && let Some(ref c) = cache {
469 let session_dir = c.cache_dir().join("session");
470 docs.set_session_cache_dir(session_dir);
471 session_cache_set = true;
472 }
473 total += scan_workspace(
474 root,
475 Arc::clone(&docs),
476 open_files.clone(),
477 cache,
478 &exclude_paths,
479 &include_paths,
480 max_indexed_files,
481 )
482 .await;
483 }
484
485 client
486 .send_notification::<ProgressNotification>(ProgressParams {
487 token,
488 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
489 WorkDoneProgressEnd {
490 message: Some(format!("Indexed {total} files")),
491 },
492 )),
493 })
494 .await;
495
496 client
497 .log_message(
498 MessageType::INFO,
499 format!("php-lsp: indexed {total} workspace files"),
500 )
501 .await;
502
503 send_refresh_requests(&client).await;
507
508 let salsa_docs = Arc::clone(&docs);
522 drop(docs);
523 client.send_notification::<IndexReadyNotification>(()).await;
524 drop(tokio::task::spawn_blocking(move || {
529 salsa_docs.get_workspace_index_salsa();
530 }));
531 });
532 }
533
534 self.client
535 .log_message(MessageType::INFO, "php-lsp ready")
536 .await;
537 }
538
539 async fn did_change_configuration(&self, _params: DidChangeConfigurationParams) {
540 let items = vec![ConfigurationItem {
543 scope_uri: None,
544 section: Some("php-lsp".to_string()),
545 }];
546 if let Ok(values) = self.client.configuration(items).await
547 && let Some(value) = values.into_iter().next()
548 {
549 let roots = self.root_paths.load_full();
550
551 let file_cfg = crate::autoload::load_project_config_json(&roots);
554
555 if let Some(ver) = value.get("phpVersion").and_then(|v| v.as_str())
556 && !crate::autoload::is_valid_php_version(ver)
557 {
558 self.client
559 .log_message(
560 tower_lsp::lsp_types::MessageType::WARNING,
561 format!(
562 "php-lsp: unsupported phpVersion {ver:?} — valid values: {}",
563 crate::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
564 ),
565 )
566 .await;
567 }
568
569 let file_obj = file_cfg.as_ref().filter(|v| v.is_object());
570 let merged = LspConfig::merge_project_configs(file_obj, Some(&value));
571 let mut cfg = LspConfig::from_value(&merged);
572
573 let (ver, source) = self.resolve_php_version(cfg.php_version.as_deref());
575 self.client
576 .log_message(
577 tower_lsp::lsp_types::MessageType::INFO,
578 format!("php-lsp: using PHP {ver} ({source})"),
579 )
580 .await;
581 let ver = if source != "set by editor" && !crate::autoload::is_valid_php_version(&ver) {
583 let clamped = crate::autoload::clamp_php_version(&ver);
584 self.client
585 .show_message(
586 tower_lsp::lsp_types::MessageType::WARNING,
587 format!(
588 "php-lsp: detected PHP {ver} is outside the supported range ({}); \
589 using PHP {clamped} for analysis",
590 crate::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
591 ),
592 )
593 .await;
594 clamped.to_string()
595 } else {
596 ver
597 };
598 cfg.php_version = Some(ver.clone());
599 if let Ok(pv) = ver.parse::<mir_analyzer::PhpVersion>() {
600 self.docs.set_php_version(pv);
601 }
602 self.config.store(Arc::new(cfg));
603 send_refresh_requests(&self.client).await;
604 }
605 }
606
607 async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
608 {
610 let mut roots = (**self.root_paths.load()).clone();
611 for removed in ¶ms.event.removed {
612 if let Ok(path) = removed.uri.to_file_path() {
613 roots.retain(|r| r != &path);
614 }
615 }
616 self.root_paths.store(Arc::new(roots));
617 }
618
619 let (exclude_paths, include_paths, max_indexed_files) = {
621 let cfg = self.config.load();
622 (
623 cfg.exclude_paths.clone(),
624 cfg.include_paths.clone(),
625 cfg.max_indexed_files,
626 )
627 };
628 for added in ¶ms.event.added {
629 if let Ok(path) = added.uri.to_file_path() {
630 let is_new = {
631 let mut roots = (**self.root_paths.load()).clone();
632 if !roots.contains(&path) {
633 roots.push(path.clone());
634 self.root_paths.store(Arc::new(roots));
635 true
636 } else {
637 false
638 }
639 };
640 if is_new {
641 let docs = Arc::clone(&self.docs);
642 let open_files = self.open_files.clone();
643 let ex = exclude_paths.clone();
644 let ip = include_paths.clone();
645 let path_clone = path.clone();
646 let client = self.client.clone();
647 tokio::spawn(async move {
648 let cache = crate::cache::WorkspaceCache::new(&path_clone);
649 scan_workspace(
650 path_clone,
651 docs,
652 open_files,
653 cache,
654 &ex,
655 &ip,
656 max_indexed_files,
657 )
658 .await;
659 send_refresh_requests(&client).await;
660 });
661 }
662 }
663 }
664 }
665
666 async fn shutdown(&self) -> Result<()> {
667 Ok(())
668 }
669
670 #[tracing::instrument(skip_all)]
671 async fn did_open(&self, params: DidOpenTextDocumentParams) {
672 guard_async("did_open", async move {
673 let uri = params.text_document.uri;
674 let text = params.text_document.text;
675
676 self.set_open_text(uri.clone(), text.clone());
680
681 let docs_for_spawn = Arc::clone(&self.docs);
682 let diag_cfg = self.config.load().diagnostics.clone();
683
684 let uri_sem = uri.clone();
690 let sem_issues = tokio::task::spawn_blocking(move || {
691 docs_for_spawn.get_semantic_issues_salsa(&uri_sem)
692 })
693 .await
694 .unwrap_or(None);
695
696 let parse_diags = self
699 .docs
700 .get_doc_salsa(&uri)
701 .map(|doc| crate::analysis::diagnostics::diagnostics_from_doc(&doc))
702 .unwrap_or_default();
703
704 self.set_parse_diagnostics(&uri, parse_diags.clone());
705 let semantic = sem_issues
706 .map(|issues| {
707 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &uri, &diag_cfg)
708 })
709 .unwrap_or_default();
710 let all_diags = merge_file_diagnostics(parse_diags, semantic);
711 self.client
713 .publish_diagnostics(uri.clone(), all_diags, None)
714 .await;
715
716 let dependents = self.compute_dependent_publishes(&uri, &diag_cfg).await;
720 for (dep_uri, dep_diags) in dependents {
721 self.client
722 .publish_diagnostics(dep_uri, dep_diags, None)
723 .await;
724 }
725 })
726 .await
727 }
728
729 #[tracing::instrument(skip_all)]
730 async fn did_change(&self, params: DidChangeTextDocumentParams) {
731 guard_async("did_change", async move {
732 let uri = params.text_document.uri;
733 let mut updated: Option<String> = None;
738 for change in params.content_changes {
739 match change.range {
740 None => updated = Some(change.text),
741 Some(range) => {
742 let mut cur = match updated.take() {
743 Some(t) => t,
744 None => self.get_open_text(&uri).unwrap_or_default(),
745 };
746 crate::util::apply_content_change(&mut cur, range, &change.text);
747 updated = Some(cur);
748 }
749 }
750 }
751 let Some(text) = updated else { return };
752
753 let version = self.set_open_text(uri.clone(), text.clone());
757
758 let docs = Arc::clone(&self.docs);
759 let open_files = self.open_files.clone();
760 let client = self.client.clone();
761 let diag_cfg = self.config.load().diagnostics.clone();
762 tokio::spawn(async move {
763 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
766
767 let (_doc, diagnostics) =
768 tokio::task::spawn_blocking(move || parse_document(&text))
769 .await
770 .unwrap_or_else(|_| (ParsedDoc::default(), vec![]));
771
772 if open_files.current_version(&uri) == Some(version) {
775 open_files.set_parse_diagnostics(&uri, diagnostics.clone());
776
777 let docs_sem = Arc::clone(&docs);
783 let uri_sem = uri.clone();
784 let diag_cfg_sem = diag_cfg.clone();
785 let extra_sem = tokio::task::spawn_blocking(move || {
786 docs_sem
787 .get_semantic_issues_salsa(&uri_sem)
788 .map(|issues| {
789 crate::semantic_diagnostics::issues_to_diagnostics(
790 &issues,
791 &uri_sem,
792 &diag_cfg_sem,
793 )
794 })
795 .unwrap_or_default()
796 })
797 .await
798 .unwrap_or_default();
799
800 let all_diags = merge_file_diagnostics(diagnostics, extra_sem);
801 client
806 .publish_diagnostics(uri.clone(), all_diags, None)
807 .await;
808
809 let dependents = compute_dependent_publishes_owned(
818 Arc::clone(&docs),
819 open_files.clone(),
820 uri.clone(),
821 diag_cfg.clone(),
822 )
823 .await;
824 for (dep_uri, dep_diags) in dependents {
825 client.publish_diagnostics(dep_uri, dep_diags, None).await;
826 }
827 }
828 });
829 })
830 .await
831 }
832
833 async fn did_close(&self, params: DidCloseTextDocumentParams) {
834 let uri = params.text_document.uri;
835 self.close_open_file(&uri);
836 self.client.publish_diagnostics(uri, vec![], None).await;
838 }
839
840 async fn will_save(&self, _params: WillSaveTextDocumentParams) {}
841
842 async fn will_save_wait_until(
843 &self,
844 params: WillSaveTextDocumentParams,
845 ) -> Result<Option<Vec<TextEdit>>> {
846 let source = self
847 .get_open_text(¶ms.text_document.uri)
848 .unwrap_or_default();
849 Ok(format_document(&source))
850 }
851
852 async fn did_save(&self, params: DidSaveTextDocumentParams) {
853 let uri = params.text_document.uri;
854 let diag_cfg = self.config.load().diagnostics.clone();
860 let all = compute_open_file_diagnostics(&self.docs, &self.open_files, &uri, &diag_cfg);
861 self.client.publish_diagnostics(uri, all, None).await;
862 }
863
864 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
865 for change in params.changes {
866 match change.typ {
867 FileChangeType::CREATED | FileChangeType::CHANGED => {
868 if let Ok(path) = change.uri.to_file_path()
869 && let Ok(text) = tokio::fs::read_to_string(&path).await
870 {
871 let doc = parse_document_no_diags(&text);
876 self.index_from_doc_if_not_open(change.uri.clone(), &doc);
877 }
878 }
879 FileChangeType::DELETED => {
880 self.docs.remove(&change.uri);
881 }
882 _ => {}
883 }
884 }
885 send_refresh_requests(&self.client).await;
887 }
888
889 #[tracing::instrument(skip_all)]
890 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
891 guard_async_result("completion", async move {
892 let uri = ¶ms.text_document_position.text_document.uri;
893 let position = params.text_document_position.position;
894 let source = self.get_open_text(uri).unwrap_or_default();
895 let doc = match self.get_doc(uri) {
896 Some(d) => d,
897 None => return Ok(Some(CompletionResponse::Array(vec![]))),
898 };
899 let other_docs: Vec<Arc<ParsedDoc>> = self
900 .docs
901 .other_docs(uri, &self.open_urls())
902 .into_iter()
903 .map(|(_, d)| d)
904 .collect();
905 let trigger = params
906 .context
907 .as_ref()
908 .and_then(|c| c.trigger_character.as_deref());
909 let meta_loaded = self.meta.load();
910 let meta_opt = if meta_loaded.is_empty() {
911 None
912 } else {
913 Some(&**meta_loaded)
914 };
915 let imports = self.file_imports(uri);
916 let wi = self.workspace_index_async().await;
917 let docs_for_lookup = Arc::clone(&self.docs);
918 let find_class_doc_fn = move |name: &str| -> Option<Arc<ParsedDoc>> {
919 let cr = *wi.classes_by_name.get(name)?.first()?;
920 let (uri, _) = wi.at(cr)?;
921 docs_for_lookup.get_doc_salsa(uri)
922 };
923 let analysis = self.cached_analysis_async(uri).await;
924 let docs_for_tm = Arc::clone(&self.docs);
928 let doc_for_tm = Arc::clone(&doc);
929 let uri_for_tm = uri.clone();
930 let get_type_map =
931 move || docs_for_tm.cached_type_map(&uri_for_tm, &doc_for_tm, meta_opt);
932 let ctx = CompletionCtx {
933 source: Some(&source),
934 position: Some(position),
935 meta: meta_opt,
936 doc_uri: Some(uri),
937 file_imports: Some(&imports),
938 find_class_doc: Some(&find_class_doc_fn),
939 analysis: analysis.as_deref(),
940 type_map: Some(&get_type_map),
941 };
942 Ok(Some(CompletionResponse::Array(filtered_completions_at(
943 &doc,
944 &other_docs,
945 trigger,
946 &ctx,
947 ))))
948 })
949 .await
950 }
951
952 async fn completion_resolve(&self, mut item: CompletionItem) -> Result<CompletionItem> {
953 if item.documentation.is_some() && item.detail.is_some() {
954 return Ok(item);
955 }
956 let name = item.label.trim_end_matches(':');
958 let all_indexes = self.docs.all_indexes();
959 if item.detail.is_none()
960 && let Some(sig) = signature_for_symbol_from_index(name, &all_indexes)
961 {
962 item.detail = Some(sig);
963 }
964 if item.documentation.is_none()
965 && let Some(md) = docs_for_symbol_from_index(name, &all_indexes)
966 {
967 item.documentation = Some(Documentation::MarkupContent(MarkupContent {
968 kind: MarkupKind::Markdown,
969 value: md,
970 }));
971 }
972 Ok(item)
973 }
974
975 async fn goto_definition(
976 &self,
977 params: GotoDefinitionParams,
978 ) -> Result<Option<GotoDefinitionResponse>> {
979 guard_async_result("goto_definition", async move {
980 let uri = ¶ms.text_document_position_params.text_document.uri;
981 let position = params.text_document_position_params.position;
982 let source = self.get_open_text(uri).unwrap_or_default();
983 let doc = match self.get_doc(uri) {
984 Some(d) => d,
985 None => return Ok(None),
986 };
987 if let Some(word) = crate::util::word_at_position(&source, position)
992 && !word.starts_with('$')
993 {
994 let analysis = self.cached_analysis_async(uri).await;
995 let resolved_class = analysis.as_deref().and_then(|a| {
996 let off = crate::util::word_range_at(&source, position)
997 .map(|r| doc.view().byte_of_position(r.start))?;
998 let sym = a.symbol_at(off)?;
999 match &sym.kind {
1000 mir_analyzer::ReferenceKind::MethodCall { class, .. }
1001 | mir_analyzer::ReferenceKind::StaticCall { class, .. } => {
1002 Some(fqn_short_name(class).to_string())
1003 }
1004 _ => None,
1005 }
1006 });
1007 if let Some(cls) = resolved_class {
1008 let all_indexes = self.docs.all_indexes();
1009 if let Some(loc) = find_method_in_class_hierarchy(&cls, &word, &all_indexes) {
1010 let refined = self
1011 .docs
1012 .get_doc_salsa(&loc.uri)
1013 .and_then(|d| {
1014 let range = find_method_range_in_class(&d, &cls, &word)
1021 .or_else(|| find_declaration_range(d.source(), &d, &word));
1022 range.map(|range| Location {
1023 uri: loc.uri.clone(),
1024 range,
1025 })
1026 })
1027 .unwrap_or(loc);
1028 return Ok(Some(GotoDefinitionResponse::Scalar(refined)));
1029 }
1030 }
1031 }
1032
1033 if let Some(loc) = goto_definition(uri, &source, &doc, &[], position) {
1035 return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
1036 }
1037 if let Some(line_text) = source.lines().nth(position.line as usize)
1041 && let Some(word) = crate::util::word_at_position(&source, position)
1042 && let Some(receiver) = crate::hover::extract_receiver_var_before_cursor(
1043 line_text,
1044 position.character as usize,
1045 )
1046 {
1047 let class_name = if receiver == "$this" {
1048 crate::type_map::enclosing_class_at(&source, &doc, position)
1049 } else {
1050 let tm = crate::type_map::TypeMap::from_doc_at_position(&doc, None, position);
1051 tm.get(&receiver).map(|s| s.to_string())
1052 };
1053 if let Some(cls) = class_name {
1054 let first_cls = cls.split('|').next().unwrap_or(&cls).to_owned();
1055 let all_indexes = self.docs.all_indexes();
1056 if let Some(loc) =
1057 find_method_in_class_hierarchy(&first_cls, &word, &all_indexes)
1058 {
1059 let refined = self
1060 .docs
1061 .get_doc_salsa(&loc.uri)
1062 .and_then(|doc| {
1063 find_declaration_range(doc.source(), &doc, &word).map(|range| {
1064 Location {
1065 uri: loc.uri.clone(),
1066 range,
1067 }
1068 })
1069 })
1070 .unwrap_or(loc);
1071 return Ok(Some(GotoDefinitionResponse::Scalar(refined)));
1072 }
1073 }
1074 }
1075
1076 let wi = self.workspace_index_async().await;
1081 if let Some(word) = crate::util::word_at_position(&source, position)
1082 && let Some(loc) = wi.find_declaration(&word, Some(uri))
1083 {
1084 let refined = self
1085 .docs
1086 .get_doc_salsa(&loc.uri)
1087 .and_then(|doc| {
1088 find_declaration_range(doc.source(), &doc, &word).map(|range| Location {
1089 uri: loc.uri.clone(),
1090 range,
1091 })
1092 })
1093 .unwrap_or(loc);
1094 return Ok(Some(GotoDefinitionResponse::Scalar(refined)));
1095 }
1096
1097 if let Some(word) = word_at_position(&source, position)
1100 && word.contains('\\')
1101 {
1102 let imports = crate::navigation::references::collect_class_imports(&doc);
1104 let expanded = expand_alias_prefix(&word, &imports);
1105 if let Some(loc) = self.psr4_goto(&expanded).await {
1106 return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
1107 }
1108 }
1109
1110 Ok(None)
1111 })
1112 .await
1113 }
1114
1115 async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
1116 guard_async_result("references", async move {
1117 tokio::task::yield_now().await;
1123 let uri = ¶ms.text_document_position.text_document.uri;
1124 let position = params.text_document_position.position;
1125 let source = self.get_open_text(uri).unwrap_or_default();
1126 let word = match word_at_position(&source, position) {
1127 Some(w) => w,
1128 None => return Ok(None),
1129 };
1130 let include_declaration = params.context.include_declaration;
1131
1132 if word == "__construct"
1136 && let Some(doc) = self.get_doc(uri)
1137 && let Some(class_name) =
1138 class_name_at_construct_decl(doc.source(), &doc.program().stmts, position)
1139 {
1140 let locations = self.construct_references(
1141 uri,
1142 &source,
1143 position,
1144 &class_name,
1145 include_declaration,
1146 );
1147 return Ok((!locations.is_empty()).then_some(locations));
1148 }
1149
1150 let doc_opt = self.get_doc(uri);
1151 let (word, kind, constant_owner) =
1152 resolve_reference_symbol(doc_opt.as_ref(), &source, position, word);
1153 let all_docs = self.docs.all_docs_for_scan();
1154 let target_fqn = self.resolve_reference_target_fqn(
1155 uri,
1156 doc_opt.as_ref(),
1157 &word,
1158 kind,
1159 position,
1160 constant_owner,
1161 );
1162
1163 if matches!(kind, Some(SymbolKind::Method)) {
1168 let docs = Arc::clone(&self.docs);
1173 let _ = tokio::task::spawn_blocking(move || docs.ensure_all_files_ingested()).await;
1174 }
1175 let owner_short: Option<String> = if matches!(kind, Some(SymbolKind::Method)) {
1176 target_fqn
1177 .as_deref()
1178 .map(|fqn| fqn_short_name(fqn.trim_start_matches('\\')).to_string())
1179 } else {
1180 None
1181 };
1182
1183 let session_method_refs = self.session_method_references(
1184 &word,
1185 kind,
1186 target_fqn.as_deref(),
1187 owner_short.as_deref(),
1188 );
1189
1190 let mut locations = if let Some(session_locs) =
1191 session_method_refs.filter(|l| !l.is_empty())
1192 {
1193 let mut combined = session_locs;
1199 if include_declaration {
1200 let range =
1201 crate::util::word_range_at(&source, position).unwrap_or_else(|| Range {
1202 start: position,
1203 end: Position {
1204 line: position.line,
1205 character: position.character + word.len() as u32,
1206 },
1207 });
1208 combined.push(Location {
1209 uri: uri.clone(),
1210 range,
1211 });
1212 crate::references::dedup_ref_locations(&mut combined);
1213 }
1214 combined
1215 } else {
1216 match target_fqn.as_deref() {
1217 Some(t) => {
1218 find_references_with_target(&word, &all_docs, include_declaration, kind, t)
1219 }
1220 None => find_references(&word, &all_docs, include_declaration, kind),
1221 }
1222 };
1223
1224 if !matches!(kind, Some(SymbolKind::Method) | Some(SymbolKind::Property))
1230 && let Some(sym) = build_mir_symbol(&word, kind, target_fqn.as_deref())
1231 {
1232 let extra = self.docs.session_references_to(&sym);
1233 if !extra.is_empty() {
1234 let mut seen: std::collections::HashSet<(String, u32, u32, u32)> = locations
1235 .iter()
1236 .map(crate::references::ref_location_key)
1237 .collect();
1238 for loc in extra
1239 .into_iter()
1240 .filter_map(crate::references::session_tuple_to_location)
1241 {
1242 if seen.insert(crate::references::ref_location_key(&loc)) {
1243 locations.push(loc);
1244 }
1245 }
1246 }
1247 }
1248
1249 Ok((!locations.is_empty()).then_some(locations))
1250 })
1251 .await
1252 }
1253
1254 async fn prepare_rename(
1255 &self,
1256 params: TextDocumentPositionParams,
1257 ) -> Result<Option<PrepareRenameResponse>> {
1258 let uri = ¶ms.text_document.uri;
1259 let source = self.get_open_text(uri).unwrap_or_default();
1260 Ok(prepare_rename(&source, params.position).map(PrepareRenameResponse::Range))
1261 }
1262
1263 async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
1264 let uri = ¶ms.text_document_position.text_document.uri;
1265 let position = params.text_document_position.position;
1266 let source = self.get_open_text(uri).unwrap_or_default();
1267 let word = match word_at_position(&source, position) {
1268 Some(w) => w,
1269 None => return Ok(None),
1270 };
1271 if word.starts_with('$') {
1272 let doc = match self.get_doc(uri) {
1273 Some(d) => d,
1274 None => return Ok(None),
1275 };
1276 Ok(Some(rename_variable(
1277 &word,
1278 ¶ms.new_name,
1279 uri,
1280 &doc,
1281 position,
1282 )))
1283 } else if is_after_arrow(&source, position) {
1284 let all_docs = self.docs.all_docs_for_scan();
1285 Ok(Some(rename_property(&word, ¶ms.new_name, &all_docs)))
1286 } else {
1287 let all_docs = self.docs.all_docs_for_scan();
1288 let doc_opt = self.get_doc(uri);
1289 let target_fqn: Option<String> = doc_opt.as_ref().map(|doc| {
1290 let imports = self.file_imports(uri);
1291 crate::navigation::moniker::resolve_fqn(doc, &word, &imports)
1292 });
1293 Ok(Some(rename(
1294 &word,
1295 ¶ms.new_name,
1296 &all_docs,
1297 target_fqn.as_deref(),
1298 )))
1299 }
1300 }
1301
1302 async fn signature_help(&self, params: SignatureHelpParams) -> Result<Option<SignatureHelp>> {
1303 let uri = ¶ms.text_document_position_params.text_document.uri;
1304 let position = params.text_document_position_params.position;
1305 let source = self.get_open_text(uri).unwrap_or_default();
1306 let doc = match self.get_doc(uri) {
1307 Some(d) => d,
1308 None => return Ok(None),
1309 };
1310 let all_indexes = self.docs.all_indexes();
1311 Ok(signature_help(&source, &doc, position, &all_indexes))
1312 }
1313
1314 #[tracing::instrument(skip_all)]
1315 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
1316 guard_async_result("hover", async move {
1317 let uri = ¶ms.text_document_position_params.text_document.uri;
1318 let position = params.text_document_position_params.position;
1319 let source = self.get_open_text(uri).unwrap_or_default();
1320 let doc = match self.get_doc(uri) {
1321 Some(d) => d,
1322 None => return Ok(None),
1323 };
1324 let other_docs = self.docs.other_docs(uri, &self.open_urls());
1325 let other_maps = self.docs.other_symbol_maps(uri, &self.open_urls());
1326 let analysis = self.cached_analysis_async(uri).await;
1327 let result = hover_info_with_maps(
1328 &source,
1329 &doc,
1330 analysis.as_deref(),
1331 position,
1332 &other_docs,
1333 &other_maps,
1334 );
1335 if result.is_some() {
1336 return Ok(result);
1337 }
1338 if let Some(word) = crate::util::word_at_position(&source, position) {
1343 let wi = self.workspace_index_async().await;
1344 if let Some(h) = class_hover_from_index(&word, &wi.files) {
1346 return Ok(Some(h));
1347 }
1348 if let Some(resolved) = crate::hover::resolve_use_alias(&doc.program().stmts, &word)
1350 && let Some(h) = class_hover_from_index(&resolved, &wi.files)
1351 {
1352 return Ok(Some(h));
1353 }
1354 }
1355 Ok(None)
1356 })
1357 .await
1358 }
1359
1360 async fn document_symbol(
1361 &self,
1362 params: DocumentSymbolParams,
1363 ) -> Result<Option<DocumentSymbolResponse>> {
1364 let uri = ¶ms.text_document.uri;
1365 let doc = match self.get_doc(uri) {
1366 Some(d) => d,
1367 None => return Ok(None),
1368 };
1369 Ok(Some(DocumentSymbolResponse::Nested(document_symbols(
1370 doc.source(),
1371 &doc,
1372 ))))
1373 }
1374
1375 async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
1376 let uri = ¶ms.text_document.uri;
1377 let doc = match self.get_doc(uri) {
1378 Some(d) => d,
1379 None => return Ok(None),
1380 };
1381 let ranges = folding_ranges(doc.source(), &doc);
1382 Ok(if ranges.is_empty() {
1383 None
1384 } else {
1385 Some(ranges)
1386 })
1387 }
1388
1389 async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
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 analysis = self.cached_analysis_async(uri).await;
1396 let wi = self.workspace_index_async().await;
1397 Ok(Some(inlay_hints(
1398 doc.source(),
1399 &doc,
1400 analysis.as_deref(),
1401 params.range,
1402 &wi.files,
1403 )))
1404 }
1405
1406 async fn inlay_hint_resolve(&self, mut item: InlayHint) -> Result<InlayHint> {
1407 if item.tooltip.is_some() {
1408 return Ok(item);
1409 }
1410 let func_name = item
1411 .data
1412 .as_ref()
1413 .and_then(|d| d.get("php_lsp_fn"))
1414 .and_then(|v| v.as_str())
1415 .map(str::to_string);
1416 if let Some(name) = func_name {
1417 let all_indexes = self.docs.all_indexes();
1418 if let Some(md) = docs_for_symbol_from_index(&name, &all_indexes) {
1419 item.tooltip = Some(InlayHintTooltip::MarkupContent(MarkupContent {
1420 kind: MarkupKind::Markdown,
1421 value: md,
1422 }));
1423 }
1424 }
1425 Ok(item)
1426 }
1427
1428 async fn symbol(
1429 &self,
1430 params: WorkspaceSymbolParams,
1431 ) -> Result<Option<Vec<SymbolInformation>>> {
1432 let wi = self.workspace_index_async().await;
1436 let results = workspace_symbols_from_workspace(¶ms.query, &wi);
1437 Ok(Some(results))
1438 }
1439
1440 async fn symbol_resolve(&self, params: WorkspaceSymbol) -> Result<WorkspaceSymbol> {
1441 let docs = self.docs.docs_for(&self.open_urls());
1443 Ok(resolve_workspace_symbol(params, &docs))
1444 }
1445
1446 #[tracing::instrument(skip_all)]
1447 async fn semantic_tokens_full(
1448 &self,
1449 params: SemanticTokensParams,
1450 ) -> Result<Option<SemanticTokensResult>> {
1451 guard_async_result("semantic_tokens_full", async move {
1452 let uri = ¶ms.text_document.uri;
1453 let doc = match self.get_doc(uri) {
1454 Some(d) => d,
1455 None => {
1456 return Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
1457 result_id: None,
1458 data: vec![],
1459 })));
1460 }
1461 };
1462 let tokens = semantic_tokens(doc.source(), &doc);
1463 let result_id = token_hash(&tokens);
1464 let tokens_arc = Arc::new(tokens);
1465 self.docs
1466 .store_token_cache(uri, result_id.clone(), Arc::clone(&tokens_arc));
1467 let data = Arc::try_unwrap(tokens_arc).unwrap_or_else(|arc| (*arc).clone());
1468 Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
1469 result_id: Some(result_id),
1470 data,
1471 })))
1472 })
1473 .await
1474 }
1475
1476 async fn semantic_tokens_range(
1477 &self,
1478 params: SemanticTokensRangeParams,
1479 ) -> Result<Option<SemanticTokensRangeResult>> {
1480 let uri = ¶ms.text_document.uri;
1481 let doc = match self.get_doc(uri) {
1482 Some(d) => d,
1483 None => {
1484 return Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
1485 result_id: None,
1486 data: vec![],
1487 })));
1488 }
1489 };
1490 let tokens = semantic_tokens_range(doc.source(), &doc, params.range);
1491 Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
1492 result_id: None,
1493 data: tokens,
1494 })))
1495 }
1496
1497 async fn semantic_tokens_full_delta(
1498 &self,
1499 params: SemanticTokensDeltaParams,
1500 ) -> Result<Option<SemanticTokensFullDeltaResult>> {
1501 let uri = ¶ms.text_document.uri;
1502 let doc = match self.get_doc(uri) {
1503 Some(d) => d,
1504 None => return Ok(None),
1505 };
1506
1507 let new_tokens = Arc::new(semantic_tokens(doc.source(), &doc));
1508 let new_result_id = token_hash(&new_tokens);
1509 let prev_id = ¶ms.previous_result_id;
1510
1511 let result = match self.docs.get_token_cache(uri, prev_id) {
1512 Some(old_tokens) => {
1513 let edits = compute_token_delta(&old_tokens, &new_tokens);
1514 SemanticTokensFullDeltaResult::TokensDelta(SemanticTokensDelta {
1515 result_id: Some(new_result_id.clone()),
1516 edits,
1517 })
1518 }
1519 None => SemanticTokensFullDeltaResult::Tokens(SemanticTokens {
1521 result_id: Some(new_result_id.clone()),
1522 data: (*new_tokens).clone(),
1523 }),
1524 };
1525
1526 self.docs.store_token_cache(uri, new_result_id, new_tokens);
1527 Ok(Some(result))
1528 }
1529
1530 async fn selection_range(
1531 &self,
1532 params: SelectionRangeParams,
1533 ) -> Result<Option<Vec<SelectionRange>>> {
1534 let uri = ¶ms.text_document.uri;
1535 let doc = match self.get_doc(uri) {
1536 Some(d) => d,
1537 None => return Ok(None),
1538 };
1539 let ranges = selection_ranges(&doc, ¶ms.positions);
1540 Ok(if ranges.is_empty() {
1541 None
1542 } else {
1543 Some(ranges)
1544 })
1545 }
1546
1547 async fn prepare_call_hierarchy(
1548 &self,
1549 params: CallHierarchyPrepareParams,
1550 ) -> Result<Option<Vec<CallHierarchyItem>>> {
1551 let uri = ¶ms.text_document_position_params.text_document.uri;
1552 let position = params.text_document_position_params.position;
1553 let source = self.get_open_text(uri).unwrap_or_default();
1554 let word = match word_at_position(&source, position) {
1555 Some(w) => w,
1556 None => return Ok(None),
1557 };
1558 let wi = self.workspace_index_async().await;
1561 let docs = Arc::clone(&self.docs);
1562 let get_doc = move |u: &Url| docs.get_doc_salsa(u);
1563 Ok(prepare_call_hierarchy_indexed(&word, &wi, &get_doc).map(|item| vec![item]))
1564 }
1565
1566 async fn incoming_calls(
1567 &self,
1568 params: CallHierarchyIncomingCallsParams,
1569 ) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
1570 let docs = Arc::clone(&self.docs);
1573 let item = params.item;
1574 let calls = tokio::task::spawn_blocking(move || {
1575 let all_docs = docs.all_docs_for_scan();
1576 incoming_calls(&item, &all_docs)
1577 })
1578 .await
1579 .unwrap_or_default();
1580 Ok(if calls.is_empty() { None } else { Some(calls) })
1581 }
1582
1583 async fn outgoing_calls(
1584 &self,
1585 params: CallHierarchyOutgoingCallsParams,
1586 ) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
1587 let wi = self.workspace_index_async().await;
1590 let docs = Arc::clone(&self.docs);
1591 let item = params.item;
1592 let calls = tokio::task::spawn_blocking(move || {
1593 let get_doc = |u: &Url| docs.get_doc_salsa(u);
1594 outgoing_calls_indexed(&item, &wi, &get_doc)
1595 })
1596 .await
1597 .unwrap_or_default();
1598 Ok(if calls.is_empty() { None } else { Some(calls) })
1599 }
1600
1601 async fn document_highlight(
1602 &self,
1603 params: DocumentHighlightParams,
1604 ) -> Result<Option<Vec<DocumentHighlight>>> {
1605 let uri = ¶ms.text_document_position_params.text_document.uri;
1606 let position = params.text_document_position_params.position;
1607 let source = self.get_open_text(uri).unwrap_or_default();
1608 let doc = match self.get_doc(uri) {
1609 Some(d) => d,
1610 None => return Ok(None),
1611 };
1612 let highlights = document_highlights(&source, &doc, position);
1613 Ok(if highlights.is_empty() {
1614 None
1615 } else {
1616 Some(highlights)
1617 })
1618 }
1619
1620 async fn linked_editing_range(
1621 &self,
1622 params: LinkedEditingRangeParams,
1623 ) -> Result<Option<LinkedEditingRanges>> {
1624 let uri = ¶ms.text_document_position_params.text_document.uri;
1625 let position = params.text_document_position_params.position;
1626 let source = self.get_open_text(uri).unwrap_or_default();
1627 let doc = match self.get_doc(uri) {
1628 Some(d) => d,
1629 None => return Ok(None),
1630 };
1631 let word = match crate::util::word_at_position(&source, position) {
1635 Some(w) => w,
1636 None => return Ok(None),
1637 };
1638 let is_variable = word.starts_with('$');
1639 let cursor_word_range = match crate::util::word_range_at(&source, position) {
1640 Some(r) => r,
1641 None => return Ok(None),
1642 };
1643
1644 let highlights = document_highlights(&source, &doc, position);
1646 if highlights.is_empty() {
1647 return Ok(None);
1648 }
1649
1650 if !highlights.iter().any(|h| h.range == cursor_word_range) {
1660 return Ok(None);
1661 }
1662
1663 let scope_to_class = !is_variable
1672 && crate::type_map::enclosing_class_at(&source, &doc, position).as_deref()
1673 != Some(word.as_str());
1674 let other_class_ranges: Vec<Range> = if scope_to_class {
1675 let cursor_class = crate::type_map::enclosing_class_range_at(&doc, position);
1676 crate::type_map::collect_all_class_ranges(&doc)
1677 .into_iter()
1678 .filter(|r| Some(*r) != cursor_class)
1679 .collect()
1680 } else {
1681 Vec::new()
1682 };
1683 let ranges: Vec<Range> = highlights
1684 .into_iter()
1685 .map(|h| h.range)
1686 .filter(|r| !other_class_ranges.iter().any(|ocr| range_within(*r, *ocr)))
1687 .collect();
1688 if ranges.is_empty() {
1689 return Ok(None);
1690 }
1691
1692 let word_pattern = if is_variable {
1699 r"\$[a-zA-Z_\u00A0-\uFFFF][a-zA-Z0-9_\u00A0-\uFFFF]*".to_string()
1700 } else {
1701 r"[a-zA-Z_\u00A0-\uFFFF][a-zA-Z0-9_\u00A0-\uFFFF]*".to_string()
1702 };
1703 Ok(Some(LinkedEditingRanges {
1704 ranges,
1705 word_pattern: Some(word_pattern),
1706 }))
1707 }
1708
1709 async fn goto_implementation(
1710 &self,
1711 params: tower_lsp::lsp_types::request::GotoImplementationParams,
1712 ) -> Result<Option<tower_lsp::lsp_types::request::GotoImplementationResponse>> {
1713 let uri = ¶ms.text_document_position_params.text_document.uri;
1714 let position = params.text_document_position_params.position;
1715 let source = self.get_open_text(uri).unwrap_or_default();
1716 let imports = self.file_imports(uri);
1717 let raw_word = crate::util::word_at_position(&source, position).unwrap_or_default();
1718 let (word, fqn_owned): (String, Option<String>) = if raw_word.contains('\\') {
1723 let short = raw_word
1724 .rsplit('\\')
1725 .next()
1726 .unwrap_or(&raw_word)
1727 .to_string();
1728 let full = raw_word.trim_start_matches('\\').to_string();
1729 (short, Some(full))
1730 } else {
1731 let fqn = imports.get(&raw_word).cloned();
1732 (raw_word, fqn)
1733 };
1734 let fqn = fqn_owned.as_deref();
1735 let open_docs = self.docs.docs_for(&self.open_urls());
1737 let mut locs = find_implementations(&word, fqn, &open_docs);
1738 if locs.is_empty() {
1739 let wi = self.workspace_index_async().await;
1742 locs = find_implementations_from_workspace(&word, fqn, &wi);
1743 }
1744 if locs.is_empty() {
1745 Ok(None)
1746 } else {
1747 Ok(Some(GotoDefinitionResponse::Array(locs)))
1748 }
1749 }
1750
1751 async fn goto_declaration(
1752 &self,
1753 params: tower_lsp::lsp_types::request::GotoDeclarationParams,
1754 ) -> Result<Option<tower_lsp::lsp_types::request::GotoDeclarationResponse>> {
1755 let uri = ¶ms.text_document_position_params.text_document.uri;
1756 let position = params.text_document_position_params.position;
1757 let source = self.get_open_text(uri).unwrap_or_default();
1758 let open_docs = self.docs.docs_for(&self.open_urls());
1760 if let Some(loc) = goto_declaration(&source, &open_docs, position) {
1761 return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
1762 }
1763 let all_indexes = self.docs.all_indexes();
1765 Ok(goto_declaration_from_index(&source, &all_indexes, position)
1766 .map(GotoDefinitionResponse::Scalar))
1767 }
1768
1769 async fn goto_type_definition(
1770 &self,
1771 params: tower_lsp::lsp_types::request::GotoTypeDefinitionParams,
1772 ) -> Result<Option<tower_lsp::lsp_types::request::GotoTypeDefinitionResponse>> {
1773 let uri = ¶ms.text_document_position_params.text_document.uri;
1774 let position = params.text_document_position_params.position;
1775 let source = self.get_open_text(uri).unwrap_or_default();
1776 let doc = match self.get_doc(uri) {
1777 Some(d) => d,
1778 None => return Ok(None),
1779 };
1780 let analysis = self.cached_analysis_async(uri).await;
1781 let open_docs = self.docs.docs_for(&self.open_urls());
1783 let mut results =
1784 goto_type_definition(&source, &doc, analysis.as_deref(), &open_docs, position);
1785
1786 if results.is_empty() {
1788 let all_indexes = self.docs.all_indexes();
1789 results = goto_type_definition_from_index(
1790 &source,
1791 &doc,
1792 analysis.as_deref(),
1793 &all_indexes,
1794 position,
1795 );
1796 }
1797
1798 let response = match results.len() {
1800 0 => None,
1801 1 => Some(GotoDefinitionResponse::Scalar(
1802 results.into_iter().next().unwrap(),
1803 )),
1804 _ => Some(GotoDefinitionResponse::Array(results)),
1805 };
1806 Ok(response)
1807 }
1808
1809 async fn prepare_type_hierarchy(
1810 &self,
1811 params: TypeHierarchyPrepareParams,
1812 ) -> Result<Option<Vec<TypeHierarchyItem>>> {
1813 let uri = ¶ms.text_document_position_params.text_document.uri;
1814 let position = params.text_document_position_params.position;
1815 let source = self.get_open_text(uri).unwrap_or_default();
1816 let wi = self.workspace_index_async().await;
1818 Ok(prepare_type_hierarchy_from_workspace(&source, &wi, position).map(|item| vec![item]))
1819 }
1820
1821 async fn supertypes(
1822 &self,
1823 params: TypeHierarchySupertypesParams,
1824 ) -> Result<Option<Vec<TypeHierarchyItem>>> {
1825 let wi = self.workspace_index_async().await;
1827 let result = supertypes_of_from_workspace(¶ms.item, &wi);
1828 Ok(if result.is_empty() {
1829 None
1830 } else {
1831 Some(result)
1832 })
1833 }
1834
1835 async fn subtypes(
1836 &self,
1837 params: TypeHierarchySubtypesParams,
1838 ) -> Result<Option<Vec<TypeHierarchyItem>>> {
1839 let wi = self.workspace_index_async().await;
1841 let result = subtypes_of_from_workspace(¶ms.item, &wi);
1842 Ok(if result.is_empty() {
1843 None
1844 } else {
1845 Some(result)
1846 })
1847 }
1848
1849 async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
1850 let uri = ¶ms.text_document.uri;
1851 let doc = match self.get_doc(uri) {
1852 Some(d) => d,
1853 None => return Ok(None),
1854 };
1855 let docs = Arc::clone(&self.docs);
1858 let uri_owned = uri.clone();
1859 let lenses = tokio::task::spawn_blocking(move || {
1860 let all_docs = docs.all_docs_for_scan();
1861 code_lenses(&uri_owned, &doc, &all_docs)
1862 })
1863 .await
1864 .unwrap_or_default();
1865 Ok(if lenses.is_empty() {
1866 None
1867 } else {
1868 Some(lenses)
1869 })
1870 }
1871
1872 async fn code_lens_resolve(&self, params: CodeLens) -> Result<CodeLens> {
1873 Ok(params)
1875 }
1876
1877 async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
1878 let uri = ¶ms.text_document.uri;
1879 let doc = match self.get_doc(uri) {
1880 Some(d) => d,
1881 None => return Ok(None),
1882 };
1883 let links = document_links(uri, &doc, doc.source());
1884 Ok(if links.is_empty() { None } else { Some(links) })
1885 }
1886
1887 async fn document_link_resolve(&self, params: DocumentLink) -> Result<DocumentLink> {
1888 Ok(params)
1890 }
1891
1892 async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
1893 let uri = ¶ms.text_document.uri;
1894 let source = self.get_open_text(uri).unwrap_or_default();
1895 Ok(format_document(&source))
1896 }
1897
1898 async fn range_formatting(
1899 &self,
1900 params: DocumentRangeFormattingParams,
1901 ) -> Result<Option<Vec<TextEdit>>> {
1902 let uri = ¶ms.text_document.uri;
1903 let source = self.get_open_text(uri).unwrap_or_default();
1904 Ok(format_range(&source, params.range))
1905 }
1906
1907 async fn on_type_formatting(
1908 &self,
1909 params: DocumentOnTypeFormattingParams,
1910 ) -> Result<Option<Vec<TextEdit>>> {
1911 let uri = ¶ms.text_document_position.text_document.uri;
1912 let source = self.get_open_text(uri).unwrap_or_default();
1913 let edits = on_type_format(
1914 &source,
1915 params.text_document_position.position,
1916 ¶ms.ch,
1917 ¶ms.options,
1918 );
1919 Ok(if edits.is_empty() { None } else { Some(edits) })
1920 }
1921
1922 async fn execute_command(
1923 &self,
1924 params: ExecuteCommandParams,
1925 ) -> Result<Option<serde_json::Value>> {
1926 match params.command.as_str() {
1927 "php-lsp.runTest" => {
1928 let file_uri = params
1930 .arguments
1931 .first()
1932 .and_then(|v| v.as_str())
1933 .and_then(|s| Url::parse(s).ok());
1934 let filter = params
1935 .arguments
1936 .get(1)
1937 .and_then(|v| v.as_str())
1938 .unwrap_or("")
1939 .to_string();
1940
1941 let root = self.root_paths.load().first().cloned();
1942 let client = self.client.clone();
1943
1944 tokio::spawn(async move {
1945 run_phpunit(&client, &filter, root.as_deref(), file_uri.as_ref()).await;
1946 });
1947
1948 Ok(None)
1949 }
1950 _ => Ok(None),
1951 }
1952 }
1953
1954 async fn will_rename_files(&self, params: RenameFilesParams) -> Result<Option<WorkspaceEdit>> {
1955 let psr4 = self.psr4.load();
1956 let all_docs = self.docs.all_docs_for_scan();
1957 let mut merged_changes: std::collections::HashMap<
1958 tower_lsp::lsp_types::Url,
1959 Vec<tower_lsp::lsp_types::TextEdit>,
1960 > = std::collections::HashMap::new();
1961
1962 for file_rename in ¶ms.files {
1963 let old_path = tower_lsp::lsp_types::Url::parse(&file_rename.old_uri)
1964 .ok()
1965 .and_then(|u| u.to_file_path().ok());
1966 let new_path = tower_lsp::lsp_types::Url::parse(&file_rename.new_uri)
1967 .ok()
1968 .and_then(|u| u.to_file_path().ok());
1969
1970 let (Some(old_path), Some(new_path)) = (old_path, new_path) else {
1971 continue;
1972 };
1973
1974 let old_fqn = psr4.file_to_fqn(&old_path);
1975 let new_fqn = psr4.file_to_fqn(&new_path);
1976
1977 let (Some(old_fqn), Some(new_fqn)) = (old_fqn, new_fqn) else {
1978 continue;
1979 };
1980
1981 let edit = use_edits_for_rename(&old_fqn, &new_fqn, &all_docs);
1982 if let Some(changes) = edit.changes {
1983 for (uri, edits) in changes {
1984 merged_changes.entry(uri).or_default().extend(edits);
1985 }
1986 }
1987 }
1988
1989 Ok(if merged_changes.is_empty() {
1990 None
1991 } else {
1992 Some(WorkspaceEdit {
1993 changes: Some(merged_changes),
1994 ..Default::default()
1995 })
1996 })
1997 }
1998
1999 async fn did_rename_files(&self, params: RenameFilesParams) {
2000 for file_rename in ¶ms.files {
2001 if let Ok(old_uri) = tower_lsp::lsp_types::Url::parse(&file_rename.old_uri) {
2003 self.docs.remove(&old_uri);
2004 }
2005 if let Ok(new_uri) = tower_lsp::lsp_types::Url::parse(&file_rename.new_uri)
2007 && let Ok(path) = new_uri.to_file_path()
2008 && let Ok(text) = tokio::fs::read_to_string(&path).await
2009 {
2010 self.index_if_not_open(new_uri, &text);
2011 }
2012 }
2013 }
2014
2015 async fn will_create_files(&self, params: CreateFilesParams) -> Result<Option<WorkspaceEdit>> {
2018 let psr4 = self.psr4.load();
2019 let mut changes: std::collections::HashMap<Url, Vec<TextEdit>> =
2020 std::collections::HashMap::new();
2021
2022 for file in ¶ms.files {
2023 let Ok(uri) = Url::parse(&file.uri) else {
2024 continue;
2025 };
2026 if !uri.path().ends_with(".php") {
2029 continue;
2030 }
2031
2032 let stub = if let Ok(path) = uri.to_file_path()
2033 && let Some(fqn) = psr4.file_to_fqn(&path)
2034 {
2035 let (ns, class_name) = match fqn.rfind('\\') {
2036 Some(pos) => (&fqn[..pos], &fqn[pos + 1..]),
2037 None => ("", fqn.as_str()),
2038 };
2039 if ns.is_empty() {
2040 format!("<?php\n\ndeclare(strict_types=1);\n\nclass {class_name}\n{{\n}}\n")
2041 } else {
2042 format!(
2043 "<?php\n\ndeclare(strict_types=1);\n\nnamespace {ns};\n\nclass {class_name}\n{{\n}}\n"
2044 )
2045 }
2046 } else {
2047 "<?php\n\n".to_string()
2048 };
2049
2050 changes.insert(
2051 uri,
2052 vec![TextEdit {
2053 range: Range {
2054 start: Position {
2055 line: 0,
2056 character: 0,
2057 },
2058 end: Position {
2059 line: 0,
2060 character: 0,
2061 },
2062 },
2063 new_text: stub,
2064 }],
2065 );
2066 }
2067
2068 Ok(if changes.is_empty() {
2069 None
2070 } else {
2071 Some(WorkspaceEdit {
2072 changes: Some(changes),
2073 ..Default::default()
2074 })
2075 })
2076 }
2077
2078 async fn did_create_files(&self, params: CreateFilesParams) {
2079 for file in ¶ms.files {
2080 if let Ok(uri) = Url::parse(&file.uri)
2081 && let Ok(path) = uri.to_file_path()
2082 && let Ok(text) = tokio::fs::read_to_string(&path).await
2083 {
2084 self.index_if_not_open(uri, &text);
2085 }
2086 }
2087 send_refresh_requests(&self.client).await;
2088 }
2089
2090 async fn will_delete_files(&self, params: DeleteFilesParams) -> Result<Option<WorkspaceEdit>> {
2095 let psr4 = self.psr4.load();
2096 let all_docs = self.docs.all_docs_for_scan();
2097 let mut merged_changes: std::collections::HashMap<Url, Vec<TextEdit>> =
2098 std::collections::HashMap::new();
2099
2100 for file in ¶ms.files {
2101 let path = Url::parse(&file.uri)
2102 .ok()
2103 .and_then(|u| u.to_file_path().ok());
2104 let Some(path) = path else { continue };
2105 let Some(fqn) = psr4.file_to_fqn(&path) else {
2106 continue;
2107 };
2108
2109 let edit = use_edits_for_delete(&fqn, &all_docs);
2110 if let Some(changes) = edit.changes {
2111 for (uri, edits) in changes {
2112 merged_changes.entry(uri).or_default().extend(edits);
2113 }
2114 }
2115 }
2116
2117 Ok(if merged_changes.is_empty() {
2118 None
2119 } else {
2120 Some(WorkspaceEdit {
2121 changes: Some(merged_changes),
2122 ..Default::default()
2123 })
2124 })
2125 }
2126
2127 async fn did_delete_files(&self, params: DeleteFilesParams) {
2128 for file in ¶ms.files {
2129 if let Ok(uri) = Url::parse(&file.uri) {
2130 self.docs.remove(&uri);
2131 self.client.publish_diagnostics(uri, vec![], None).await;
2133 }
2134 }
2135 send_refresh_requests(&self.client).await;
2136 }
2137
2138 async fn moniker(&self, params: MonikerParams) -> Result<Option<Vec<Moniker>>> {
2141 let uri = ¶ms.text_document_position_params.text_document.uri;
2142 let position = params.text_document_position_params.position;
2143 let source = self.get_open_text(uri).unwrap_or_default();
2144 let doc = match self.get_doc(uri) {
2145 Some(d) => d,
2146 None => return Ok(None),
2147 };
2148 let imports = self.file_imports(uri);
2149 Ok(moniker_at(&source, &doc, position, &imports).map(|m| vec![m]))
2150 }
2151
2152 async fn inline_value(&self, params: InlineValueParams) -> Result<Option<Vec<InlineValue>>> {
2155 let uri = ¶ms.text_document.uri;
2156 let source = self.get_open_text(uri).unwrap_or_default();
2157 let values = inline_values_in_range(&source, params.range);
2158 Ok(if values.is_empty() {
2159 None
2160 } else {
2161 Some(values)
2162 })
2163 }
2164
2165 async fn diagnostic(
2166 &self,
2167 params: DocumentDiagnosticParams,
2168 ) -> Result<DocumentDiagnosticReportResult> {
2169 let uri = ¶ms.text_document.uri;
2170 let parse_diags = self.get_parse_diagnostics(uri).unwrap_or_default();
2171 let _doc = match self.get_doc(uri) {
2172 Some(d) => d,
2173 None => {
2174 let _version = self
2176 .open_files
2177 .all_with_diagnostics()
2178 .iter()
2179 .find(|(u, _, _)| u == uri)
2180 .and_then(|(_, _, v)| *v)
2181 .unwrap_or(1);
2182 let result_id = compute_diagnostic_result_id(&parse_diags, uri.as_str());
2183 return Ok(DocumentDiagnosticReportResult::Report(
2184 DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
2185 related_documents: None,
2186 full_document_diagnostic_report: FullDocumentDiagnosticReport {
2187 result_id: Some(result_id),
2188 items: parse_diags,
2189 },
2190 }),
2191 ));
2192 }
2193 };
2194 let (diag_cfg, php_version) = {
2195 let cfg = self.config.load();
2196 (cfg.diagnostics.clone(), cfg.php_version.clone())
2197 };
2198 let _ = php_version;
2200
2201 let docs = Arc::clone(&self.docs);
2203 let uri_owned = uri.clone();
2204 let diag_cfg_sem = diag_cfg.clone();
2205 let sem_diags = tokio::task::spawn_blocking(move || {
2206 docs.get_semantic_issues_salsa(&uri_owned)
2207 .map(|issues| {
2208 crate::semantic_diagnostics::issues_to_diagnostics(
2209 &issues,
2210 &uri_owned,
2211 &diag_cfg_sem,
2212 )
2213 })
2214 .unwrap_or_default()
2215 })
2216 .await
2217 .map_err(|e| {
2218 use std::borrow::Cow;
2219 tower_lsp::jsonrpc::Error {
2220 code: tower_lsp::jsonrpc::ErrorCode::InternalError,
2221 message: Cow::Owned(format!("diagnostic analysis failed: {}", e)),
2222 data: None,
2223 }
2224 })?;
2225
2226 let items = merge_file_diagnostics(parse_diags, sem_diags);
2227
2228 let _version = self
2230 .open_files
2231 .all_with_diagnostics()
2232 .iter()
2233 .find(|(u, _, _)| u == uri)
2234 .and_then(|(_, _, v)| *v)
2235 .unwrap_or(1);
2236 let result_id = compute_diagnostic_result_id(&items, uri.as_str());
2237
2238 Ok(DocumentDiagnosticReportResult::Report(
2239 DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
2240 related_documents: None,
2241 full_document_diagnostic_report: FullDocumentDiagnosticReport {
2242 result_id: Some(result_id),
2243 items,
2244 },
2245 }),
2246 ))
2247 }
2248
2249 async fn workspace_diagnostic(
2250 &self,
2251 params: WorkspaceDiagnosticParams,
2252 ) -> Result<WorkspaceDiagnosticReportResult> {
2253 let all_parse_diags = self.all_open_files_with_diagnostics();
2254 let (diag_cfg, php_version) = {
2255 let cfg = self.config.load();
2256 (cfg.diagnostics.clone(), cfg.php_version.clone())
2257 };
2258
2259 let _ = php_version;
2261
2262 let previous_map: std::collections::HashMap<Url, String> = params
2268 .previous_result_ids
2269 .into_iter()
2270 .map(|p| (p.uri, p.value))
2271 .collect();
2272
2273 let docs = Arc::clone(&self.docs);
2281 let diag_cfg_sweep = diag_cfg.clone();
2282 let items = tokio::task::spawn_blocking(move || {
2283 all_parse_diags
2284 .into_iter()
2285 .map(|(uri, parse_diags, version)| {
2286 let sem_diags = docs
2287 .get_semantic_issues_salsa(&uri)
2288 .map(|issues| {
2289 crate::semantic_diagnostics::issues_to_diagnostics(
2290 &issues,
2291 &uri,
2292 &diag_cfg_sweep,
2293 )
2294 })
2295 .unwrap_or_default();
2296 let all_diags = merge_file_diagnostics(parse_diags, sem_diags);
2297
2298 let result_id = compute_diagnostic_result_id(&all_diags, uri.as_str());
2299
2300 if previous_map.get(&uri) == Some(&result_id) {
2303 WorkspaceDocumentDiagnosticReport::Unchanged(
2304 WorkspaceUnchangedDocumentDiagnosticReport {
2305 uri,
2306 version,
2307 unchanged_document_diagnostic_report:
2308 UnchangedDocumentDiagnosticReport { result_id },
2309 },
2310 )
2311 } else {
2312 WorkspaceDocumentDiagnosticReport::Full(
2313 WorkspaceFullDocumentDiagnosticReport {
2314 uri,
2315 version,
2316 full_document_diagnostic_report: FullDocumentDiagnosticReport {
2317 result_id: Some(result_id),
2318 items: all_diags,
2319 },
2320 },
2321 )
2322 }
2323 })
2324 .collect::<Vec<_>>()
2325 })
2326 .await
2327 .map_err(|e| {
2328 use std::borrow::Cow;
2329 tower_lsp::jsonrpc::Error {
2330 code: tower_lsp::jsonrpc::ErrorCode::InternalError,
2331 message: Cow::Owned(format!("workspace_diagnostic analysis failed: {}", e)),
2332 data: None,
2333 }
2334 })?;
2335
2336 Ok(WorkspaceDiagnosticReportResult::Report(
2337 WorkspaceDiagnosticReport { items },
2338 ))
2339 }
2340
2341 async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
2342 let uri = ¶ms.text_document.uri;
2343 let source = self.get_open_text(uri).unwrap_or_default();
2344 let doc = match self.get_doc(uri) {
2345 Some(d) => d,
2346 None => return Ok(None),
2347 };
2348 let other_docs = self.docs.other_docs(uri, &self.open_urls());
2349
2350 let diag_cfg = self.config.load().diagnostics.clone();
2357 let docs_sem = Arc::clone(&self.docs);
2358 let uri_sem = uri.clone();
2359 let diag_cfg_sem = diag_cfg.clone();
2360 let sem_diags = tokio::task::spawn_blocking(move || {
2361 docs_sem
2362 .get_semantic_issues_salsa(&uri_sem)
2363 .map(|issues| {
2364 crate::semantic_diagnostics::issues_to_diagnostics(
2365 &issues,
2366 &uri_sem,
2367 &diag_cfg_sem,
2368 )
2369 })
2370 .unwrap_or_default()
2371 })
2372 .await
2373 .unwrap_or_default();
2374
2375 let mut actions: Vec<CodeActionOrCommand> = Vec::new();
2377 for diag in &sem_diags {
2378 if diag.code != Some(NumberOrString::String("UndefinedClass".to_string())) {
2379 continue;
2380 }
2381 if diag.range.start.line < params.range.start.line
2383 || diag.range.start.line > params.range.end.line
2384 {
2385 continue;
2386 }
2387 let class_name = diag
2389 .message
2390 .strip_prefix("Class ")
2391 .and_then(|s| s.strip_suffix(" does not exist"))
2392 .unwrap_or("")
2393 .trim();
2394 if class_name.is_empty() {
2395 continue;
2396 }
2397
2398 for (_other_uri, other_doc) in &other_docs {
2400 if let Some(fqn) = find_fqn_for_class(other_doc, class_name) {
2401 let edit = build_use_import_edit(&source, uri, &fqn);
2402 let action = CodeAction {
2403 title: format!("Add use {fqn}"),
2404 kind: Some(CodeActionKind::QUICKFIX),
2405 edit: Some(edit),
2406 diagnostics: Some(vec![diag.clone()]),
2407 ..Default::default()
2408 };
2409 actions.push(CodeActionOrCommand::CodeAction(action));
2410 break; }
2412 }
2413 }
2414
2415 for tag in DEFERRED_ACTION_TAGS {
2418 actions.extend(defer_actions(
2419 self.generate_deferred_actions(tag, &source, &doc, params.range, uri),
2420 tag,
2421 uri,
2422 params.range,
2423 ));
2424 }
2425
2426 actions.extend(extract_variable_actions(&source, params.range, uri));
2428 actions.extend(extract_method_actions(&source, &doc, params.range, uri));
2429 actions.extend(extract_constant_actions(&source, params.range, uri));
2430 actions.extend(inline_variable_actions(&source, params.range, uri));
2432 if let Some(action) = organize_imports_action(&source, uri) {
2434 actions.push(action);
2435 }
2436
2437 Ok(if actions.is_empty() {
2438 None
2439 } else {
2440 Some(actions)
2441 })
2442 }
2443
2444 async fn code_action_resolve(&self, item: CodeAction) -> Result<CodeAction> {
2445 let data = match &item.data {
2446 Some(d) => d.clone(),
2447 None => return Ok(item),
2448 };
2449 let kind_tag = match data.get("php_lsp_resolve").and_then(|v| v.as_str()) {
2450 Some(k) => k.to_string(),
2451 None => return Ok(item),
2452 };
2453 let uri: Url = match data
2454 .get("uri")
2455 .and_then(|v| v.as_str())
2456 .and_then(|s| Url::parse(s).ok())
2457 {
2458 Some(u) => u,
2459 None => return Ok(item),
2460 };
2461 let range: Range = match data
2462 .get("range")
2463 .and_then(|v| serde_json::from_value(v.clone()).ok())
2464 {
2465 Some(r) => r,
2466 None => return Ok(item),
2467 };
2468
2469 let source = self.get_open_text(&uri).unwrap_or_default();
2470 let doc = match self.get_doc(&uri) {
2471 Some(d) => d,
2472 None => return Ok(item),
2473 };
2474
2475 let candidates = self.generate_deferred_actions(&kind_tag, &source, &doc, range, &uri);
2476
2477 for candidate in candidates {
2479 if let CodeActionOrCommand::CodeAction(ca) = candidate
2480 && ca.title == item.title
2481 {
2482 return Ok(ca);
2483 }
2484 }
2485
2486 Ok(item)
2487 }
2488}
2489
2490fn expand_alias_prefix(word: &str, imports: &std::collections::HashMap<String, String>) -> String {
2497 if let Some((first, rest)) = word.split_once('\\')
2498 && let Some(ns_prefix) = imports.get(first)
2499 {
2500 return format!("{}\\{}", ns_prefix, rest);
2501 }
2502 word.to_string()
2503}