use std::sync::Arc;
use tower_lsp_server::ls_types::*;
use crate::document::ast::ParsedDoc;
use crate::document::document_store::DocumentStore;
use crate::navigation::definition::find_declaration_range;
use crate::actions::generate_action::{
generate_constructor_actions, generate_getters_setters_actions,
};
use crate::actions::implement_action::implement_missing_actions;
use crate::actions::phpdoc_action::phpdoc_actions;
use crate::actions::promote_action::promote_constructor_actions;
use crate::actions::type_action::add_return_type_actions;
use super::Backend;
mod cursor_decl;
mod phpunit;
mod position;
pub(super) use cursor_decl::*;
pub(super) use phpunit::*;
pub(super) use position::*;
pub(super) fn indexed_file_op(extensions: &[String]) -> FileOperationRegistrationOptions {
FileOperationRegistrationOptions {
filters: extensions
.iter()
.map(|ext| FileOperationFilter {
scheme: Some("file".to_string()),
pattern: FileOperationPattern {
glob: format!("**/*.{ext}"),
matches: Some(FileOperationPatternKind::File),
options: None,
},
})
.collect(),
}
}
pub(super) fn has_indexed_extension(path: &str, extensions: &[String]) -> bool {
extensions
.iter()
.any(|ext| path.ends_with(&format!(".{ext}")))
}
pub(super) fn defer_actions(
actions: Vec<CodeActionOrCommand>,
kind_tag: &str,
uri: &Uri,
range: Range,
) -> Vec<CodeActionOrCommand> {
actions
.into_iter()
.map(|a| match a {
CodeActionOrCommand::CodeAction(mut ca) => {
ca.edit = None;
ca.data = Some(serde_json::json!({
"php_lsp_resolve": kind_tag,
"uri": uri.to_string(),
"range": range,
}));
CodeActionOrCommand::CodeAction(ca)
}
other => other,
})
.collect()
}
pub(super) const DEFERRED_ACTION_TAGS: &[&str] = &[
"phpdoc",
"implement",
"constructor",
"getters_setters",
"return_type",
"promote",
];
impl Backend {
pub(super) async fn cached_analysis_async(
&self,
uri: &Uri,
) -> Option<Arc<mir_analyzer::FileAnalysis>> {
if let Some(hit) = self.docs.cached_analysis_if_fresh(uri) {
return Some(hit);
}
let docs = Arc::clone(&self.docs);
let uri_owned = uri.clone();
match tokio::task::spawn_blocking(move || docs.cached_analysis(&uri_owned)).await {
Ok(r) => r,
Err(e) => {
tracing::warn!("cached_analysis panicked for {uri:?}: {e}");
None
}
}
}
pub(super) async fn cached_analysis_async_cancellable(
&self,
uri: &Uri,
cancel_rev: u64,
) -> Option<Arc<mir_analyzer::FileAnalysis>> {
if let Some(hit) = self.docs.cached_analysis_if_fresh(uri) {
return Some(hit);
}
let docs = Arc::clone(&self.docs);
let docs_cancel = Arc::clone(&self.docs);
let uri_owned = uri.clone();
match tokio::task::spawn_blocking(move || {
docs.cached_analysis_cancellable(&uri_owned, &|| docs_cancel.write_rev() != cancel_rev)
})
.await
{
Ok(r) => r,
Err(e) => {
tracing::warn!("cached_analysis panicked for {uri:?}: {e}");
None
}
}
}
pub(super) async fn workspace_index_async(
&self,
) -> Arc<crate::db::workspace_index::WorkspaceIndexData> {
let docs = Arc::clone(&self.docs);
match tokio::task::spawn_blocking(move || docs.get_workspace_index_salsa()).await {
Ok(wi) => wi,
Err(_) => self.docs.get_workspace_index_salsa(),
}
}
pub(super) async fn workspace_index_cached(
&self,
slot: &mut Option<Arc<crate::db::workspace_index::WorkspaceIndexData>>,
) -> Arc<crate::db::workspace_index::WorkspaceIndexData> {
if let Some(wi) = slot {
return Arc::clone(wi);
}
let wi = self.workspace_index_async().await;
*slot = Some(Arc::clone(&wi));
wi
}
pub(super) async fn rescan_roots_after_version_change(&self, roots: &[std::path::PathBuf]) {
let (exclude_paths, include_paths, max_indexed_files, cache_path) = {
let cfg = self.config.load();
let mut exclude = cfg.exclude_paths.clone();
if !cfg.index_vendor && !exclude.iter().any(|p| p == "vendor" || p == "vendor/") {
exclude.push("vendor/".to_string());
}
(
exclude,
cfg.include_paths.clone(),
cfg.max_indexed_files,
cfg.cache_path.clone(),
)
};
let extensions = self.config.load().php_file_extensions();
for root in roots {
let cache = if let Some(ref p) = cache_path {
Some(crate::index::cache::WorkspaceCache::with_dir(p.clone()))
} else {
crate::index::cache::WorkspaceCache::new(root)
};
crate::index::workspace_scan::scan_workspace(
root.clone(),
Arc::clone(&self.docs),
self.open_files.clone(),
cache,
&exclude_paths,
&include_paths,
&extensions,
max_indexed_files,
None,
)
.await;
}
let docs = Arc::clone(&self.docs);
let open_urls = self.open_files.urls();
let warm_analysis = self.config.load().warm_analysis;
let _ = tokio::task::spawn_blocking(move || {
docs.get_workspace_index_salsa();
let untrusted = docs.warm_start_indexes();
docs.warm_references_phase(untrusted, &open_urls, warm_analysis, None);
})
.await;
}
pub(super) async fn psr4_goto(&self, fqn: &str) -> Option<Location> {
let psr4 = self.psr4.load();
let path = psr4.resolve(fqn)?;
let file_uri = Uri::from_file_path(&path)?;
if self.docs.get_doc_salsa(&file_uri).is_none() {
let text = tokio::fs::read_to_string(&path).await.ok()?;
self.ingest_if_not_open(file_uri.clone(), &text);
}
let doc = self.docs.get_doc_salsa(&file_uri)?;
let short_name = fqn.split('\\').next_back()?;
let range = find_declaration_range(&doc, short_name)?;
Some(Location {
uri: file_uri,
range,
})
}
pub(super) async fn ensure_direct_supertypes_loaded(
&self,
item_fqn: &str,
wi: &crate::db::workspace_index::WorkspaceIndexData,
) -> bool {
let Some(class_ref) = self.docs.resolve_class_ref_by_fqn(wi, item_fqn) else {
return false;
};
let mut ingested = false;
{
let Some((uri, cls)) = wi.at(class_ref) else {
return false;
};
let Some(doc) = self.docs.get_doc_salsa(uri) else {
return false;
};
let imports = doc.file_imports();
let mut super_names: Vec<String> = Vec::new();
if let Some(p) = &cls.parent {
super_names.push(p.as_ref().to_owned());
}
for iface in &cls.implements {
super_names.push(iface.as_ref().to_owned());
}
for name in super_names {
let fqn = crate::navigation::moniker::resolve_fqn(&doc, &name, &imports);
if self.docs.resolve_class_ref_by_fqn(wi, &fqn).is_some() {
continue;
}
let path = match self.psr4.load().resolve(&fqn) {
Some(p) => p,
None => continue,
};
let uri = match Uri::from_file_path(&path) {
Some(u) => u,
None => continue,
};
if self.docs.get_doc_salsa(&uri).is_some() {
continue;
}
let text = match tokio::fs::read_to_string(&path).await {
Ok(t) => t,
Err(_) => continue,
};
self.ingest_if_not_open(uri, &text);
ingested = true;
}
}
ingested
}
}
pub(super) fn generate_deferred_actions(
docs: &Arc<DocumentStore>,
tag: &str,
source: &str,
doc: &Arc<ParsedDoc>,
range: Range,
uri: &Uri,
) -> Vec<CodeActionOrCommand> {
match tag {
"phpdoc" => phpdoc_actions(uri, doc, source, range),
"implement" => {
let imports = doc.file_imports();
let needles = crate::actions::implement_action::target_type_names(
&doc.program().stmts,
doc.view(),
range,
);
let all_docs = if needles.is_empty() {
Vec::new()
} else {
docs.docs_for_scan_mentioning(&needles)
};
implement_missing_actions(source, doc, &all_docs, range, uri, &imports)
}
"constructor" => generate_constructor_actions(source, doc, range, uri),
"getters_setters" => generate_getters_setters_actions(source, doc, range, uri),
"return_type" => add_return_type_actions(source, doc, range, uri),
"promote" => promote_constructor_actions(source, doc, range, uri),
_ => Vec::new(),
}
}