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 = uri.clone();
100 tokio::task::spawn_blocking(move || docs.cached_analysis(&uri))
101 .await
102 .unwrap_or(None)
103 }
104
105 pub(super) async fn workspace_index_async(
110 &self,
111 ) -> Arc<crate::db::workspace_index::WorkspaceIndexData> {
112 let docs = Arc::clone(&self.docs);
113 match tokio::task::spawn_blocking(move || docs.get_workspace_index_salsa()).await {
114 Ok(wi) => wi,
115 Err(_) => self.docs.get_workspace_index_salsa(),
118 }
119 }
120
121 pub(super) fn generate_deferred_actions(
123 &self,
124 tag: &str,
125 source: &str,
126 doc: &Arc<ParsedDoc>,
127 range: Range,
128 uri: &Url,
129 ) -> Vec<CodeActionOrCommand> {
130 match tag {
131 "phpdoc" => phpdoc_actions(uri, doc, source, range),
132 "implement" => {
133 let imports = self.file_imports(uri);
134 implement_missing_actions(
135 source,
136 doc,
137 &self.docs.all_docs_for_scan(),
138 range,
139 uri,
140 &imports,
141 )
142 }
143 "constructor" => generate_constructor_actions(source, doc, range, uri),
144 "getters_setters" => generate_getters_setters_actions(source, doc, range, uri),
145 "return_type" => add_return_type_actions(source, doc, range, uri),
146 "promote" => promote_constructor_actions(source, doc, range, uri),
147 _ => Vec::new(),
148 }
149 }
150
151 pub(super) async fn psr4_goto(&self, fqn: &str) -> Option<Location> {
154 let path = self.psr4.load().resolve(fqn)?;
155
156 let file_uri = Url::from_file_path(&path).ok()?;
157
158 if self.docs.get_doc_salsa(&file_uri).is_none() {
163 let text = tokio::fs::read_to_string(&path).await.ok()?;
164 self.ingest_if_not_open(file_uri.clone(), &text);
165 }
166
167 let doc = self.docs.get_doc_salsa(&file_uri)?;
168
169 let short_name = fqn.split('\\').next_back()?;
172 let range = find_declaration_range(doc.source(), &doc, short_name)?;
173
174 Some(Location {
175 uri: file_uri,
176 range,
177 })
178 }
179
180 pub(super) async fn psr4_method_goto(
187 &self,
188 class_fqn: &str,
189 method_name: &str,
190 ) -> Option<Location> {
191 use crate::index::file_index::FileIndex;
192 use crate::navigation::definition::{find_declaration_range, find_method_range_in_class};
193 use crate::text::zero_width_range;
194 use std::collections::{HashSet, VecDeque};
195
196 let mut queue: VecDeque<String> = VecDeque::from([class_fqn.to_owned()]);
197 let mut visited: HashSet<String> = HashSet::new();
198
199 while let Some(fqn) = queue.pop_front() {
200 if !visited.insert(fqn.clone()) {
201 continue;
202 }
203
204 let path = match self.psr4.load().resolve(&fqn) {
205 Some(p) => p,
206 None => continue,
207 };
208 let uri = match Url::from_file_path(&path) {
209 Ok(u) => u,
210 Err(_) => continue,
211 };
212
213 if self.docs.get_doc_salsa(&uri).is_none() {
215 let text = match tokio::fs::read_to_string(&path).await {
216 Ok(t) => t,
217 Err(_) => continue,
218 };
219 self.ingest_if_not_open(uri.clone(), &text);
220 }
221
222 let doc = match self.docs.get_doc_salsa(&uri) {
223 Some(d) => d,
224 None => continue,
225 };
226
227 let index = self.docs.get_vendor_index(&uri).unwrap_or_else(|| {
229 let idx = Arc::new(FileIndex::extract(&doc));
230 self.docs.cache_vendor_index(uri.clone(), Arc::clone(&idx));
231 idx
232 });
233
234 let short = crate::text::fqn_short_name(&fqn);
235
236 for cls in &index.classes {
237 if cls.name.as_ref() != short {
238 continue;
239 }
240
241 for m in &cls.methods {
242 if m.name.as_ref() == method_name {
243 let range = find_method_range_in_class(&doc, short, method_name)
244 .or_else(|| find_declaration_range(doc.source(), &doc, method_name))
245 .unwrap_or_else(|| zero_width_range(m.start_line));
246 return Some(Location { uri, range });
247 }
248 }
249 for dm in &cls.doc_methods {
250 if dm.name.as_ref() == method_name {
251 return Some(Location {
252 uri,
253 range: zero_width_range(dm.start_line),
254 });
255 }
256 }
257
258 for trt in &cls.traits {
260 queue.push_back(resolve_name_to_fqn(trt.as_ref(), &index));
261 }
262 for mx in &cls.mixins {
263 queue.push_back(resolve_name_to_fqn(mx.as_ref(), &index));
264 }
265 if let Some(parent) = &cls.parent {
266 queue.push_back(resolve_name_to_fqn(parent.as_ref(), &index));
267 }
268 }
269 }
270 None
271 }
272
273 pub(super) async fn ensure_direct_supertypes_loaded(
279 &self,
280 item_name: &str,
281 wi: &crate::db::workspace_index::WorkspaceIndexData,
282 ) -> bool {
283 let refs = match wi.classes_by_name.get(item_name) {
284 Some(r) => r.clone(),
285 None => return false,
286 };
287
288 let mut ingested = false;
289 for r in &refs {
290 let Some((_, cls)) = wi.at(*r) else {
291 continue;
292 };
293 let file_idx = wi.files.get(r.file as usize).map(|(_, idx)| idx.as_ref());
294
295 let mut super_names: Vec<String> = Vec::new();
296 if let Some(p) = &cls.parent {
297 super_names.push(p.as_ref().to_owned());
298 }
299 for iface in &cls.implements {
300 super_names.push(iface.as_ref().to_owned());
301 }
302
303 for name in super_names {
304 let short = crate::text::fqn_short_name(&name);
305 if wi.classes_by_name.contains_key(short) {
306 continue;
307 }
308 let fqn = if let Some(idx) = file_idx {
310 resolve_name_to_fqn(&name, idx)
311 } else {
312 name.clone()
313 };
314 let path = match self.psr4.load().resolve(&fqn) {
315 Some(p) => p,
316 None => continue,
317 };
318 let uri = match Url::from_file_path(&path) {
319 Ok(u) => u,
320 Err(_) => continue,
321 };
322 if self.docs.get_doc_salsa(&uri).is_some() {
323 continue;
324 }
325 let text = match tokio::fs::read_to_string(&path).await {
326 Ok(t) => t,
327 Err(_) => continue,
328 };
329 self.ingest_if_not_open(uri, &text);
330 ingested = true;
331 }
332 }
333 ingested
334 }
335
336 pub async fn apply_workspace_edit(&self, edit: WorkspaceEdit) -> bool {
339 self.client
340 .apply_edit(edit)
341 .await
342 .ok()
343 .map(|result| result.applied)
344 .unwrap_or(false)
345 }
346}
347
348fn resolve_name_to_fqn(name: &str, index: &crate::index::file_index::FileIndex) -> String {
353 if name.contains('\\') {
355 return name.trim_start_matches('\\').to_owned();
356 }
357 for (alias, fqn) in &index.use_imports {
359 if alias.as_ref() == name {
360 return fqn.as_ref().trim_start_matches('\\').to_owned();
361 }
362 }
363 if let Some(ns) = &index.namespace {
365 return format!("{}\\{}", ns.trim_start_matches('\\'), name);
366 }
367 name.to_owned()
368}