1use std::sync::Arc;
11
12use tower_lsp::lsp_types::*;
13
14use crate::document::ast::ParsedDoc;
15use crate::navigation::definition::find_declaration_range;
16
17use crate::actions::generate_action::{
18 generate_constructor_actions, generate_getters_setters_actions,
19};
20use crate::actions::implement_action::implement_missing_actions;
21use crate::actions::phpdoc_action::phpdoc_actions;
22use crate::actions::promote_action::promote_constructor_actions;
23use crate::actions::type_action::add_return_type_actions;
24
25use super::Backend;
26
27mod cursor_decl;
28mod phpunit;
29mod position;
30
31pub(super) use cursor_decl::*;
32pub(super) use phpunit::*;
33pub(super) use position::*;
34
35pub(super) fn php_file_op() -> FileOperationRegistrationOptions {
36 FileOperationRegistrationOptions {
37 filters: vec![FileOperationFilter {
38 scheme: Some("file".to_string()),
39 pattern: FileOperationPattern {
40 glob: "**/*.php".to_string(),
41 matches: Some(FileOperationPatternKind::File),
42 options: None,
43 },
44 }],
45 }
46}
47
48pub(super) fn defer_actions(
51 actions: Vec<CodeActionOrCommand>,
52 kind_tag: &str,
53 uri: &Url,
54 range: Range,
55) -> Vec<CodeActionOrCommand> {
56 actions
57 .into_iter()
58 .map(|a| match a {
59 CodeActionOrCommand::CodeAction(mut ca) => {
60 ca.edit = None;
61 ca.data = Some(serde_json::json!({
62 "php_lsp_resolve": kind_tag,
63 "uri": uri.to_string(),
64 "range": range,
65 }));
66 CodeActionOrCommand::CodeAction(ca)
67 }
68 other => other,
69 })
70 .collect()
71}
72
73pub(super) const DEFERRED_ACTION_TAGS: &[&str] = &[
76 "phpdoc",
77 "implement",
78 "constructor",
79 "getters_setters",
80 "return_type",
81 "promote",
82];
83
84impl Backend {
85 pub(super) async fn cached_analysis_async(
92 &self,
93 uri: &Url,
94 ) -> Option<Arc<mir_analyzer::FileAnalysis>> {
95 if let Some(hit) = self.docs.cached_analysis_if_fresh(uri) {
96 return Some(hit);
97 }
98 let docs = Arc::clone(&self.docs);
99 let uri_owned = uri.clone();
100 match tokio::task::spawn_blocking(move || docs.cached_analysis(&uri_owned)).await {
101 Ok(r) => r,
102 Err(e) => {
103 tracing::warn!("cached_analysis panicked for {uri}: {e}");
104 None
105 }
106 }
107 }
108
109 pub(super) async fn workspace_index_async(
114 &self,
115 ) -> Arc<crate::db::workspace_index::WorkspaceIndexData> {
116 let docs = Arc::clone(&self.docs);
117 match tokio::task::spawn_blocking(move || docs.get_workspace_index_salsa()).await {
118 Ok(wi) => wi,
119 Err(_) => self.docs.get_workspace_index_salsa(),
122 }
123 }
124
125 pub(super) fn generate_deferred_actions(
127 &self,
128 tag: &str,
129 source: &str,
130 doc: &Arc<ParsedDoc>,
131 range: Range,
132 uri: &Url,
133 ) -> Vec<CodeActionOrCommand> {
134 match tag {
135 "phpdoc" => phpdoc_actions(uri, doc, source, range),
136 "implement" => {
137 let imports = self.file_imports(uri);
138 implement_missing_actions(
139 source,
140 doc,
141 &self.docs.all_docs_for_scan(),
142 range,
143 uri,
144 &imports,
145 )
146 }
147 "constructor" => generate_constructor_actions(source, doc, range, uri),
148 "getters_setters" => generate_getters_setters_actions(source, doc, range, uri),
149 "return_type" => add_return_type_actions(source, doc, range, uri),
150 "promote" => promote_constructor_actions(source, doc, range, uri),
151 _ => Vec::new(),
152 }
153 }
154
155 pub(super) async fn psr4_goto(&self, fqn: &str) -> Option<Location> {
158 let psr4 = self.psr4.load();
159 let path = psr4.resolve(fqn).or_else(|| psr4.psr0_resolve(fqn))?;
160
161 let file_uri = Url::from_file_path(&path).ok()?;
162
163 if self.docs.get_doc_salsa(&file_uri).is_none() {
168 let text = tokio::fs::read_to_string(&path).await.ok()?;
169 self.ingest_if_not_open(file_uri.clone(), &text);
170 }
171
172 let doc = self.docs.get_doc_salsa(&file_uri)?;
173
174 let short_name = fqn.split('\\').next_back()?;
177 let range = find_declaration_range(doc.source(), &doc, short_name)?;
178
179 Some(Location {
180 uri: file_uri,
181 range,
182 })
183 }
184
185 pub(super) async fn psr4_method_goto(
192 &self,
193 class_fqn: &str,
194 method_name: &str,
195 ) -> Option<Location> {
196 use crate::index::file_index::FileIndex;
197 use crate::navigation::definition::{find_declaration_range, find_method_range_in_class};
198 use crate::text::zero_width_range;
199 use std::collections::{HashSet, VecDeque};
200
201 let mut queue: VecDeque<String> = VecDeque::from([class_fqn.to_owned()]);
202 let mut visited: HashSet<String> = HashSet::new();
203
204 while let Some(fqn) = queue.pop_front() {
205 if !visited.insert(fqn.clone()) {
206 continue;
207 }
208
209 let path = match self.psr4.load().resolve(&fqn) {
210 Some(p) => p,
211 None => continue,
212 };
213 let uri = match Url::from_file_path(&path) {
214 Ok(u) => u,
215 Err(_) => continue,
216 };
217
218 if self.docs.get_doc_salsa(&uri).is_none() {
220 let text = match tokio::fs::read_to_string(&path).await {
221 Ok(t) => t,
222 Err(_) => continue,
223 };
224 self.ingest_if_not_open(uri.clone(), &text);
225 }
226
227 let doc = match self.docs.get_doc_salsa(&uri) {
228 Some(d) => d,
229 None => continue,
230 };
231
232 let index = self.docs.get_vendor_index(&uri).unwrap_or_else(|| {
234 let idx = Arc::new(FileIndex::extract(&doc));
235 self.docs.cache_vendor_index(uri.clone(), Arc::clone(&idx));
236 idx
237 });
238
239 let short = crate::text::fqn_short_name(&fqn);
240
241 for cls in &index.classes {
242 if cls.name.as_ref() != short {
243 continue;
244 }
245
246 for m in &cls.methods {
247 if m.name.as_ref() == method_name {
248 let range = find_method_range_in_class(&doc, short, method_name)
249 .or_else(|| find_declaration_range(doc.source(), &doc, method_name))
250 .unwrap_or_else(|| zero_width_range(m.start_line));
251 return Some(Location { uri, range });
252 }
253 }
254 for dm in &cls.doc_methods {
255 if dm.name.as_ref() == method_name {
256 return Some(Location {
257 uri,
258 range: zero_width_range(dm.start_line),
259 });
260 }
261 }
262
263 for trt in &cls.traits {
265 queue.push_back(resolve_name_to_fqn(trt.as_ref(), &index));
266 }
267 for mx in &cls.mixins {
268 queue.push_back(resolve_name_to_fqn(mx.as_ref(), &index));
269 }
270 if let Some(parent) = &cls.parent {
271 queue.push_back(resolve_name_to_fqn(parent.as_ref(), &index));
272 }
273 }
274 }
275 None
276 }
277
278 pub(super) async fn ensure_direct_supertypes_loaded(
284 &self,
285 item_name: &str,
286 wi: &crate::db::workspace_index::WorkspaceIndexData,
287 ) -> bool {
288 let refs = match wi.classes_by_name.get(item_name) {
289 Some(r) => r.clone(),
290 None => return false,
291 };
292
293 let mut ingested = false;
294 for r in &refs {
295 let Some((_, cls)) = wi.at(*r) else {
296 continue;
297 };
298 let file_idx = wi.files.get(r.file as usize).map(|(_, idx)| idx.as_ref());
299
300 let mut super_names: Vec<String> = Vec::new();
301 if let Some(p) = &cls.parent {
302 super_names.push(p.as_ref().to_owned());
303 }
304 for iface in &cls.implements {
305 super_names.push(iface.as_ref().to_owned());
306 }
307
308 for name in super_names {
309 let short = crate::text::fqn_short_name(&name);
310 if wi.classes_by_name.contains_key(short) {
311 continue;
312 }
313 let fqn = if let Some(idx) = file_idx {
315 resolve_name_to_fqn(&name, idx)
316 } else {
317 name.clone()
318 };
319 let path = match self.psr4.load().resolve(&fqn) {
320 Some(p) => p,
321 None => continue,
322 };
323 let uri = match Url::from_file_path(&path) {
324 Ok(u) => u,
325 Err(_) => continue,
326 };
327 if self.docs.get_doc_salsa(&uri).is_some() {
328 continue;
329 }
330 let text = match tokio::fs::read_to_string(&path).await {
331 Ok(t) => t,
332 Err(_) => continue,
333 };
334 self.ingest_if_not_open(uri, &text);
335 ingested = true;
336 }
337 }
338 ingested
339 }
340
341 pub async fn apply_workspace_edit(&self, edit: WorkspaceEdit) -> bool {
344 self.client
345 .apply_edit(edit)
346 .await
347 .ok()
348 .map(|result| result.applied)
349 .unwrap_or(false)
350 }
351}
352
353fn resolve_name_to_fqn(name: &str, index: &crate::index::file_index::FileIndex) -> String {
358 if name.contains('\\') {
360 return name.trim_start_matches('\\').to_owned();
361 }
362 for (alias, fqn) in &index.use_imports {
364 if alias.as_ref() == name {
365 return fqn.as_ref().trim_start_matches('\\').to_owned();
366 }
367 }
368 if let Some(ns) = &index.namespace {
370 return format!("{}\\{}", ns.trim_start_matches('\\'), name);
371 }
372 name.to_owned()
373}