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 psr4 = self.psr4.load();
155 let path = psr4.resolve(fqn).or_else(|| psr4.psr0_resolve(fqn))?;
156
157 let file_uri = Url::from_file_path(&path).ok()?;
158
159 if self.docs.get_doc_salsa(&file_uri).is_none() {
164 let text = tokio::fs::read_to_string(&path).await.ok()?;
165 self.ingest_if_not_open(file_uri.clone(), &text);
166 }
167
168 let doc = self.docs.get_doc_salsa(&file_uri)?;
169
170 let short_name = fqn.split('\\').next_back()?;
173 let range = find_declaration_range(doc.source(), &doc, short_name)?;
174
175 Some(Location {
176 uri: file_uri,
177 range,
178 })
179 }
180
181 pub(super) async fn psr4_method_goto(
188 &self,
189 class_fqn: &str,
190 method_name: &str,
191 ) -> Option<Location> {
192 use crate::index::file_index::FileIndex;
193 use crate::navigation::definition::{find_declaration_range, find_method_range_in_class};
194 use crate::text::zero_width_range;
195 use std::collections::{HashSet, VecDeque};
196
197 let mut queue: VecDeque<String> = VecDeque::from([class_fqn.to_owned()]);
198 let mut visited: HashSet<String> = HashSet::new();
199
200 while let Some(fqn) = queue.pop_front() {
201 if !visited.insert(fqn.clone()) {
202 continue;
203 }
204
205 let path = match self.psr4.load().resolve(&fqn) {
206 Some(p) => p,
207 None => continue,
208 };
209 let uri = match Url::from_file_path(&path) {
210 Ok(u) => u,
211 Err(_) => continue,
212 };
213
214 if self.docs.get_doc_salsa(&uri).is_none() {
216 let text = match tokio::fs::read_to_string(&path).await {
217 Ok(t) => t,
218 Err(_) => continue,
219 };
220 self.ingest_if_not_open(uri.clone(), &text);
221 }
222
223 let doc = match self.docs.get_doc_salsa(&uri) {
224 Some(d) => d,
225 None => continue,
226 };
227
228 let index = self.docs.get_vendor_index(&uri).unwrap_or_else(|| {
230 let idx = Arc::new(FileIndex::extract(&doc));
231 self.docs.cache_vendor_index(uri.clone(), Arc::clone(&idx));
232 idx
233 });
234
235 let short = crate::text::fqn_short_name(&fqn);
236
237 for cls in &index.classes {
238 if cls.name.as_ref() != short {
239 continue;
240 }
241
242 for m in &cls.methods {
243 if m.name.as_ref() == method_name {
244 let range = find_method_range_in_class(&doc, short, method_name)
245 .or_else(|| find_declaration_range(doc.source(), &doc, method_name))
246 .unwrap_or_else(|| zero_width_range(m.start_line));
247 return Some(Location { uri, range });
248 }
249 }
250 for dm in &cls.doc_methods {
251 if dm.name.as_ref() == method_name {
252 return Some(Location {
253 uri,
254 range: zero_width_range(dm.start_line),
255 });
256 }
257 }
258
259 for trt in &cls.traits {
261 queue.push_back(resolve_name_to_fqn(trt.as_ref(), &index));
262 }
263 for mx in &cls.mixins {
264 queue.push_back(resolve_name_to_fqn(mx.as_ref(), &index));
265 }
266 if let Some(parent) = &cls.parent {
267 queue.push_back(resolve_name_to_fqn(parent.as_ref(), &index));
268 }
269 }
270 }
271 None
272 }
273
274 pub(super) async fn ensure_direct_supertypes_loaded(
280 &self,
281 item_name: &str,
282 wi: &crate::db::workspace_index::WorkspaceIndexData,
283 ) -> bool {
284 let refs = match wi.classes_by_name.get(item_name) {
285 Some(r) => r.clone(),
286 None => return false,
287 };
288
289 let mut ingested = false;
290 for r in &refs {
291 let Some((_, cls)) = wi.at(*r) else {
292 continue;
293 };
294 let file_idx = wi.files.get(r.file as usize).map(|(_, idx)| idx.as_ref());
295
296 let mut super_names: Vec<String> = Vec::new();
297 if let Some(p) = &cls.parent {
298 super_names.push(p.as_ref().to_owned());
299 }
300 for iface in &cls.implements {
301 super_names.push(iface.as_ref().to_owned());
302 }
303
304 for name in super_names {
305 let short = crate::text::fqn_short_name(&name);
306 if wi.classes_by_name.contains_key(short) {
307 continue;
308 }
309 let fqn = if let Some(idx) = file_idx {
311 resolve_name_to_fqn(&name, idx)
312 } else {
313 name.clone()
314 };
315 let path = match self.psr4.load().resolve(&fqn) {
316 Some(p) => p,
317 None => continue,
318 };
319 let uri = match Url::from_file_path(&path) {
320 Ok(u) => u,
321 Err(_) => continue,
322 };
323 if self.docs.get_doc_salsa(&uri).is_some() {
324 continue;
325 }
326 let text = match tokio::fs::read_to_string(&path).await {
327 Ok(t) => t,
328 Err(_) => continue,
329 };
330 self.ingest_if_not_open(uri, &text);
331 ingested = true;
332 }
333 }
334 ingested
335 }
336
337 pub async fn apply_workspace_edit(&self, edit: WorkspaceEdit) -> bool {
340 self.client
341 .apply_edit(edit)
342 .await
343 .ok()
344 .map(|result| result.applied)
345 .unwrap_or(false)
346 }
347}
348
349fn resolve_name_to_fqn(name: &str, index: &crate::index::file_index::FileIndex) -> String {
354 if name.contains('\\') {
356 return name.trim_start_matches('\\').to_owned();
357 }
358 for (alias, fqn) in &index.use_imports {
360 if alias.as_ref() == name {
361 return fqn.as_ref().trim_start_matches('\\').to_owned();
362 }
363 }
364 if let Some(ns) = &index.namespace {
366 return format!("{}\\{}", ns.trim_start_matches('\\'), name);
367 }
368 name.to_owned()
369}