use std::sync::Arc;
use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::*;
use tower_lsp::{LanguageServer, async_trait};
use super::panic_guard::{guard_async, guard_async_result};
use crate::completion::{CompletionCtx, filtered_completions_at};
use crate::document::ast::ParsedDoc;
use crate::document::open_files::compute_open_file_diagnostics;
use crate::hover::{
class_hover_from_index, docs_for_symbol_from_index, docs_for_symbol_from_index_scoped,
extract_static_class_before_cursor, hover_info_with_maps, method_hover_from_index,
signature_for_symbol_from_index_scoped,
};
use crate::index::file_index::ClassKind;
use crate::index::workspace_scan::{scan_workspace, send_refresh_requests};
use crate::lang::config::LspConfig;
use crate::navigation::symbols::{
document_symbols, resolve_workspace_symbol, workspace_symbols_from_workspace,
};
use crate::text::word_at_position;
use crate::navigation::call_hierarchy::{
incoming_calls, outgoing_calls_indexed, prepare_call_hierarchy_indexed,
};
use crate::navigation::declaration::{goto_declaration, goto_declaration_from_index};
use crate::navigation::implementation::{
find_implementations, find_implementations_mir_backed, find_method_implementations_mir_backed,
};
use crate::navigation::moniker::moniker_at;
use crate::navigation::type_definition::{
goto_type_definition_exact, goto_type_definition_from_index_exact,
goto_type_definition_from_index_short_name_fallback, goto_type_definition_short_name_fallback,
};
use crate::navigation::type_hierarchy::{
prepare_type_hierarchy_from_workspace, subtypes_of_mir_backed, supertypes_of_from_workspace,
};
use crate::analysis::code_lens::code_lenses;
use crate::analysis::diagnostics::{diagnostics_from_doc, parse_document, parse_document_no_diags};
use crate::analysis::document_highlight::document_highlights;
use crate::analysis::inlay_hints::inlay_hints;
use crate::analysis::inline_value::inline_values_in_range;
use crate::analysis::semantic_tokens::{
compute_token_delta, semantic_tokens, semantic_tokens_range, token_hash,
};
use crate::editing::document_link::document_links;
use crate::editing::folding::folding_ranges;
use crate::editing::formatting::{format_document, format_range};
use crate::editing::on_type_format::on_type_format;
use crate::editing::rename::{prepare_rename, rename_property, rename_variable, rename_with_kind};
use crate::editing::selection_range::selection_ranges;
use crate::editing::signature_help::signature_help;
use super::helpers::{
cursor_is_on_property_decl, is_after_arrow, promoted_property_at_cursor, run_phpunit,
symbol_kind_at,
};
use super::{Backend, publish_with_dependents};
#[async_trait]
impl LanguageServer for Backend {
async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
self.handle_initialize(params).await
}
async fn initialized(&self, _params: InitializedParams) {
guard_async("initialized", async move {
self.handle_initialized(_params).await
})
.await
}
async fn did_change_configuration(&self, _params: DidChangeConfigurationParams) {
guard_async("did_change_configuration", async move {
let items = vec![ConfigurationItem {
scope_uri: None,
section: Some("php-lsp".to_string()),
}];
if let Ok(values) = self.client.configuration(items).await
&& let Some(value) = values.into_iter().next()
{
let roots = self.root_paths.load_full();
let file_cfg = crate::lang::autoload::load_project_config_json(&roots);
if let Some(ver) = value.get("phpVersion").and_then(|v| v.as_str())
&& !crate::lang::autoload::is_valid_php_version(ver)
{
self.client
.log_message(
tower_lsp::lsp_types::MessageType::WARNING,
format!(
"php-lsp: unsupported phpVersion {ver:?} — valid values: {}",
crate::lang::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
),
)
.await;
}
let file_obj = file_cfg.as_ref().filter(|v| v.is_object());
let merged = LspConfig::merge_project_configs(file_obj, Some(&value));
let mut cfg = LspConfig::from_value(&merged);
let (ver, source) = self.resolve_php_version(cfg.php_version.as_deref());
self.client
.log_message(
tower_lsp::lsp_types::MessageType::INFO,
format!("php-lsp: using PHP {ver} ({source})"),
)
.await;
let ver = if source != "set by editor"
&& !crate::lang::autoload::is_valid_php_version(&ver)
{
let clamped = crate::lang::autoload::clamp_php_version(&ver);
self.client
.show_message(
tower_lsp::lsp_types::MessageType::WARNING,
format!(
"php-lsp: detected PHP {ver} is outside the supported range ({}); \
using PHP {clamped} for analysis",
crate::lang::autoload::SUPPORTED_PHP_VERSIONS.join(", ")
),
)
.await;
clamped.to_string()
} else {
ver
};
cfg.php_version = Some(ver.clone());
if let Ok(pv) = ver.parse::<mir_analyzer::PhpVersion>() {
self.docs.set_php_version(pv);
}
self.config.store(Arc::new(cfg));
send_refresh_requests(&self.client).await;
}
})
.await
}
async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
guard_async("did_change_workspace_folders", async move {
{
let mut roots = (**self.root_paths.load()).clone();
for removed in ¶ms.event.removed {
if let Ok(path) = removed.uri.to_file_path() {
roots.retain(|r| r != &path);
}
}
self.root_paths.store(Arc::new(roots));
}
let (exclude_paths, include_paths, max_indexed_files, cache_path) = {
let cfg = self.config.load();
(
cfg.exclude_paths.clone(),
cfg.include_paths.clone(),
cfg.max_indexed_files,
cfg.cache_path.clone(),
)
};
for added in ¶ms.event.added {
if let Ok(path) = added.uri.to_file_path() {
let is_new = {
let mut roots = (**self.root_paths.load()).clone();
if !roots.contains(&path) {
roots.push(path.clone());
self.root_paths.store(Arc::new(roots));
true
} else {
false
}
};
if is_new {
let docs = Arc::clone(&self.docs);
let open_files = self.open_files.clone();
let ex = exclude_paths.clone();
let ip = include_paths.clone();
let path_clone = path.clone();
let client = self.client.clone();
let cp = cache_path.clone();
tokio::spawn(async move {
let cache = if let Some(p) = cp {
Some(crate::index::cache::WorkspaceCache::with_dir(p))
} else {
crate::index::cache::WorkspaceCache::new(&path_clone)
};
scan_workspace(
path_clone,
docs,
open_files,
cache,
&ex,
&ip,
max_indexed_files,
None,
)
.await;
send_refresh_requests(&client).await;
});
}
}
}
})
.await
}
async fn shutdown(&self) -> Result<()> {
Ok(())
}
#[tracing::instrument(skip_all)]
async fn did_open(&self, params: DidOpenTextDocumentParams) {
guard_async("did_open", async move {
let uri = params.text_document.uri;
let text = params.text_document.text;
self.set_open_text(uri.clone(), text);
let parse_diags = self
.docs
.get_doc_salsa(&uri)
.map(|doc| diagnostics_from_doc(&doc))
.unwrap_or_default();
self.set_parse_diagnostics(&uri, parse_diags);
publish_with_dependents(
self.client.clone(),
Arc::clone(&self.docs),
self.open_files.clone(),
uri,
self.config.load().diagnostics.clone(),
)
.await;
})
.await
}
#[tracing::instrument(skip_all)]
async fn did_change(&self, params: DidChangeTextDocumentParams) {
guard_async("did_change", async move {
let uri = params.text_document.uri;
let mut updated: Option<String> = None;
for change in params.content_changes {
match change.range {
None => updated = Some(change.text),
Some(range) => {
let mut cur = match updated.take() {
Some(t) => t,
None => self.get_open_text(&uri).unwrap_or_default(),
};
crate::text::apply_content_change(&mut cur, range, &change.text);
updated = Some(cur);
}
}
}
let Some(text) = updated else { return };
let version = self.set_open_text(uri.clone(), text.clone());
let docs = Arc::clone(&self.docs);
let open_files = self.open_files.clone();
let client = self.client.clone();
let cfg = self.config.load();
let diag_cfg = cfg.diagnostics.clone();
let debounce_ms = cfg.debounce_ms;
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(debounce_ms)).await;
if open_files.current_version(&uri) != Some(version) {
return;
}
let (_doc, parse_diags) =
tokio::task::spawn_blocking(move || parse_document(&text))
.await
.unwrap_or_else(|e| {
tracing::warn!("parse_document panicked for {uri}: {e}");
(ParsedDoc::default(), vec![])
});
if open_files.current_version(&uri) == Some(version) {
open_files.set_parse_diagnostics(&uri, parse_diags);
publish_with_dependents(client, docs, open_files, uri, diag_cfg).await;
}
});
})
.await
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
guard_async("did_close", async move {
let uri = params.text_document.uri;
self.close_open_file(&uri);
let disk_text = match uri.to_file_path() {
Ok(path) => tokio::fs::read_to_string(&path).await.ok(),
Err(_) => None,
};
if !self.open_files.contains(&uri) {
match disk_text {
Some(text) => self.docs.ingest(uri.clone(), &text),
None => self.docs.remove(&uri),
}
}
self.client.publish_diagnostics(uri, vec![], None).await;
})
.await
}
async fn will_save(&self, _params: WillSaveTextDocumentParams) {}
async fn will_save_wait_until(
&self,
params: WillSaveTextDocumentParams,
) -> Result<Option<Vec<TextEdit>>> {
guard_async_result("will_save_wait_until", async move {
let source = self
.get_open_text(¶ms.text_document.uri)
.unwrap_or_default();
Ok(format_document(&source))
})
.await
}
async fn did_save(&self, params: DidSaveTextDocumentParams) {
guard_async("did_save", async move {
let uri = params.text_document.uri;
let diag_cfg = self.config.load().diagnostics.clone();
let all = compute_open_file_diagnostics(&self.docs, &self.open_files, &uri, &diag_cfg);
self.open_files
.note_published(&uri, super::diagnostics_content_hash(&all));
self.client
.publish_diagnostics(uri.clone(), all, None)
.await;
let cache_path = self.config.load().cache_path.clone();
if let Ok(path) = uri.to_file_path() {
let root = self.root_paths.load().first().cloned();
tokio::task::spawn_blocking(move || {
let cache = if let Some(p) = cache_path {
crate::index::cache::WorkspaceCache::with_dir(p)
} else if let Some(r) = root {
let Some(c) = crate::index::cache::WorkspaceCache::new(&r) else {
return;
};
c
} else {
return;
};
let Ok(text) = std::fs::read_to_string(&path) else {
return;
};
let key = crate::index::cache::WorkspaceCache::key_for(uri.as_str(), &text);
let doc = parse_document_no_diags(&text);
let index = crate::index::file_index::FileIndex::extract(&doc);
let _ = cache.write(&key, &index);
});
}
})
.await
}
async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
guard_async("did_change_watched_files", async move {
for change in params.changes {
match change.typ {
FileChangeType::CREATED | FileChangeType::CHANGED => {
if let Ok(path) = change.uri.to_file_path()
&& let Ok(text) = tokio::fs::read_to_string(&path).await
{
let doc = parse_document_no_diags(&text);
self.ingest_from_doc_if_not_open(change.uri.clone(), &doc);
}
}
FileChangeType::DELETED => {
self.docs.remove(&change.uri);
}
_ => {}
}
}
send_refresh_requests(&self.client).await;
})
.await
}
#[tracing::instrument(skip_all)]
async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
guard_async_result("completion", async move {
let uri = ¶ms.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let source = self.get_open_text(uri).unwrap_or_default();
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(Some(CompletionResponse::Array(vec![]))),
};
let other_docs: Vec<Arc<ParsedDoc>> = self
.docs
.other_docs(uri, &self.open_urls())
.into_iter()
.map(|(_, d)| d)
.collect();
let trigger = params
.context
.as_ref()
.and_then(|c| c.trigger_character.clone());
let meta_arc = self.meta.load_full();
let imports = self.file_imports(uri);
let wi = self.workspace_index_async().await;
let wi_for_class_search = Arc::clone(&wi);
let uri_for_class_search = uri.clone();
let docs_for_lookup = Arc::clone(&self.docs);
let find_class_doc_fn = move |name: &str| -> Option<Arc<ParsedDoc>> {
let cr = wi.resolve_class_ref(name)?;
let (uri, _) = wi.at(cr)?;
docs_for_lookup.get_doc_salsa(uri)
};
const WORKSPACE_CLASS_SEARCH_LIMIT: usize = 50;
let workspace_class_search_fn =
move |prefix: &str| -> Vec<(String, CompletionItemKind, String)> {
let prefix_lc = prefix.to_lowercase();
let table = &wi_for_class_search.classes_by_lowercase_name;
let start =
table.partition_point(|(name, _)| name.as_ref() < prefix_lc.as_str());
let mut out = Vec::new();
for (name, cr) in &table[start..] {
if !name.starts_with(prefix_lc.as_str()) {
break;
}
let Some((file_uri, cls)) = wi_for_class_search.at(*cr) else {
continue;
};
if *file_uri == uri_for_class_search {
continue;
}
let kind = match cls.kind {
ClassKind::Class | ClassKind::Trait => CompletionItemKind::CLASS,
ClassKind::Interface => CompletionItemKind::INTERFACE,
ClassKind::Enum => CompletionItemKind::ENUM,
};
out.push((cls.name.to_string(), kind, cls.fqn.to_string()));
if out.len() >= WORKSPACE_CLASS_SEARCH_LIMIT {
break;
}
}
out
};
let analysis = self.cached_analysis_async(uri).await;
let docs_for_tm = Arc::clone(&self.docs);
let doc_for_tm = Arc::clone(&doc);
let uri_for_tm = uri.clone();
let meta_arc_for_tm = Arc::clone(&meta_arc);
let get_type_map = move || {
let meta = if meta_arc_for_tm.is_empty() {
None
} else {
Some(&*meta_arc_for_tm)
};
docs_for_tm.cached_type_map(&uri_for_tm, &doc_for_tm, meta)
};
let session = self
.docs
.analysis_session(self.docs.workspace_php_version());
let uri_owned = uri.clone();
let uri_str = uri.to_string();
let items = match tokio::task::spawn_blocking(move || {
let meta_opt = if meta_arc.is_empty() {
None
} else {
Some(&*meta_arc)
};
let ctx = CompletionCtx {
source: Some(&source),
position: Some(position),
meta: meta_opt,
doc_uri: Some(&uri_owned),
file_imports: Some(&imports),
find_class_doc: Some(&find_class_doc_fn),
workspace_class_search: Some(&workspace_class_search_fn),
analysis: analysis.as_deref(),
type_map: Some(&get_type_map),
session: Some(session),
};
filtered_completions_at(&doc, &other_docs, trigger.as_deref(), &ctx)
})
.await
{
Ok(items) => items,
Err(e) => {
tracing::warn!("completion panicked for {uri_str}: {e}");
vec![]
}
};
Ok(Some(CompletionResponse::Array(items)))
})
.await
}
async fn completion_resolve(&self, mut item: CompletionItem) -> Result<CompletionItem> {
guard_async_result("completion_resolve", async move {
if item.documentation.is_some() && item.detail.is_some() {
return Ok(item);
}
let name = item.label.trim_end_matches(':');
let class_hint = item
.data
.as_ref()
.and_then(|d| d.get("class"))
.and_then(|v| v.as_str())
.map(str::to_string);
let all_indexes = self.docs.all_indexes();
if item.detail.is_none()
&& let Some(sig) = signature_for_symbol_from_index_scoped(
name,
&all_indexes,
class_hint.as_deref(),
)
{
item.detail = Some(sig);
}
if item.documentation.is_none()
&& let Some(md) =
docs_for_symbol_from_index_scoped(name, &all_indexes, class_hint.as_deref())
{
item.documentation = Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: md,
}));
}
Ok(item)
})
.await
}
async fn goto_definition(
&self,
params: GotoDefinitionParams,
) -> Result<Option<GotoDefinitionResponse>> {
guard_async_result("goto_definition", async move {
self.handle_goto_definition(params).await
})
.await
}
async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
guard_async_result(
"references",
async move { self.handle_references(params).await },
)
.await
}
async fn prepare_rename(
&self,
params: TextDocumentPositionParams,
) -> Result<Option<PrepareRenameResponse>> {
guard_async_result("prepare_rename", async move {
let uri = ¶ms.text_document.uri;
let source = self.get_open_text(uri).unwrap_or_default();
Ok(prepare_rename(&source, params.position).map(PrepareRenameResponse::Range))
})
.await
}
async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
guard_async_result("rename", async move {
let uri = ¶ms.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let source = self.get_open_text(uri).unwrap_or_default();
let word = match word_at_position(&source, position) {
Some(w) => w,
None => return Ok(None),
};
if word.starts_with('$') {
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(None),
};
let prop_name = cursor_is_on_property_decl(&source, &doc.program().stmts, position)
.or_else(|| {
promoted_property_at_cursor(&source, &doc.program().stmts, position)
});
if let Some(prop_name) = prop_name {
let all_docs = self.docs.all_docs_for_scan();
return Ok(Some(rename_property(
&prop_name,
¶ms.new_name,
&all_docs,
)));
}
Ok(Some(rename_variable(
&word,
¶ms.new_name,
uri,
&doc,
position,
)))
} else if is_after_arrow(&source, position) {
let all_docs = self.docs.all_docs_for_scan();
Ok(Some(rename_property(&word, ¶ms.new_name, &all_docs)))
} else {
let all_docs = self.docs.all_docs_for_scan();
let doc_opt = self.get_doc(uri);
let target_fqn: Option<String> = doc_opt.as_ref().map(|doc| {
let imports = self.file_imports(uri);
crate::navigation::moniker::resolve_fqn(doc, &word, &imports)
});
let kind = symbol_kind_at(&source, position, &word);
Ok(Some(rename_with_kind(
&word,
¶ms.new_name,
&all_docs,
kind,
target_fqn.as_deref(),
)))
}
})
.await
}
async fn signature_help(&self, params: SignatureHelpParams) -> Result<Option<SignatureHelp>> {
guard_async_result("signature_help", async move {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let source = self.get_open_text(uri).unwrap_or_default();
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(None),
};
let all_indexes = self.docs.all_indexes();
Ok(signature_help(&source, &doc, position, &all_indexes))
})
.await
}
#[tracing::instrument(skip_all)]
async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
guard_async_result("hover", async move {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let source = self.get_open_text(uri).unwrap_or_default();
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(None),
};
let other_docs = self.docs.other_docs(uri, &self.open_urls());
let other_maps = self.docs.other_symbol_maps(uri, &self.open_urls());
let analysis = self.cached_analysis_async(uri).await;
let hover_session = self
.docs
.analysis_session(self.docs.workspace_php_version());
let source_clone = source.clone();
let doc_clone = Arc::clone(&doc);
let uri_str = uri.to_string();
let result = match tokio::task::spawn_blocking(move || {
hover_info_with_maps(
&source_clone,
&doc_clone,
analysis.as_deref(),
position,
&other_docs,
&other_maps,
Some(&hover_session),
)
})
.await
{
Ok(r) => r,
Err(e) => {
tracing::warn!("hover panicked for {uri_str}: {e}");
None
}
};
if result.is_some() {
return Ok(result);
}
if let Some(word) = crate::text::word_at_position(&source, position) {
let wi = self.workspace_index_async().await;
if let Some(h) = class_hover_from_index(&word, None, &wi.files) {
return Ok(Some(h));
}
if let Some((resolved, resolved_fqn)) =
crate::hover::resolve_use_alias_fqn(&doc.program().stmts, &word)
&& let Some(h) =
class_hover_from_index(&resolved, Some(&resolved_fqn), &wi.files)
{
return Ok(Some(h));
}
if let Some(line_text) = source.lines().nth(position.line as usize)
&& let Some(class_token) =
extract_static_class_before_cursor(line_text, position.character as usize)
{
if let Some(h) = method_hover_from_index(&class_token, &word, &wi.files) {
return Ok(Some(h));
}
if let Some(resolved_class) =
crate::hover::resolve_use_alias(&doc.program().stmts, &class_token)
&& let Some(h) = method_hover_from_index(&resolved_class, &word, &wi.files)
{
return Ok(Some(h));
}
}
}
Ok(None)
})
.await
}
async fn document_symbol(
&self,
params: DocumentSymbolParams,
) -> Result<Option<DocumentSymbolResponse>> {
guard_async_result("document_symbol", async move {
let uri = ¶ms.text_document.uri;
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(None),
};
Ok(Some(DocumentSymbolResponse::Nested(document_symbols(
doc.source(),
&doc,
))))
})
.await
}
async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
guard_async_result("folding_range", async move {
let uri = ¶ms.text_document.uri;
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(None),
};
let ranges = folding_ranges(doc.source(), &doc);
Ok(if ranges.is_empty() {
None
} else {
Some(ranges)
})
})
.await
}
async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
guard_async_result("inlay_hint", async move {
let uri = ¶ms.text_document.uri;
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(None),
};
let analysis = self.cached_analysis_async(uri).await;
let wi = self.workspace_index_async().await;
let uri_str = uri.to_string();
let hints = match tokio::task::spawn_blocking(move || {
inlay_hints(
doc.source(),
&doc,
analysis.as_deref(),
params.range,
&wi.files,
)
})
.await
{
Ok(h) => h,
Err(e) => {
tracing::warn!("inlay_hint panicked for {uri_str}: {e}");
Vec::new()
}
};
Ok(Some(hints))
})
.await
}
async fn inlay_hint_resolve(&self, mut item: InlayHint) -> Result<InlayHint> {
let fallback = item.clone();
let resolved = guard_async("inlay_hint_resolve", async move {
if item.tooltip.is_some() {
return Some(item);
}
let func_name = item
.data
.as_ref()
.and_then(|d| d.get("php_lsp_fn"))
.and_then(|v| v.as_str())
.map(str::to_string);
if let Some(name) = func_name {
let all_indexes = self.docs.all_indexes();
if let Some(md) = docs_for_symbol_from_index(&name, &all_indexes) {
item.tooltip = Some(InlayHintTooltip::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: md,
}));
}
}
Some(item)
})
.await;
Ok(resolved.unwrap_or(fallback))
}
async fn symbol(
&self,
params: WorkspaceSymbolParams,
) -> Result<Option<Vec<SymbolInformation>>> {
guard_async_result("symbol", async move {
let wi = self.workspace_index_async().await;
let query = params.query;
let results = match tokio::task::spawn_blocking(move || {
workspace_symbols_from_workspace(&query, &wi)
})
.await
{
Ok(r) => r,
Err(e) => {
tracing::warn!("workspace/symbol panicked: {e}");
Vec::new()
}
};
Ok(Some(results))
})
.await
}
async fn symbol_resolve(&self, params: WorkspaceSymbol) -> Result<WorkspaceSymbol> {
let docs = self.docs.docs_for(&self.open_urls());
let fallback = params.clone();
let resolved = guard_async("symbol_resolve", async move {
Some(resolve_workspace_symbol(params, &docs))
})
.await;
Ok(resolved.unwrap_or(fallback))
}
#[tracing::instrument(skip_all)]
async fn semantic_tokens_full(
&self,
params: SemanticTokensParams,
) -> Result<Option<SemanticTokensResult>> {
guard_async_result("semantic_tokens_full", async move {
let uri = ¶ms.text_document.uri;
let doc = match self.get_doc(uri) {
Some(d) => d,
None => {
return Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
result_id: None,
data: vec![],
})));
}
};
let tokens = semantic_tokens(doc.source(), &doc);
let result_id = token_hash(&tokens);
let tokens_arc = Arc::new(tokens);
self.docs
.store_token_cache(uri, result_id.clone(), Arc::clone(&tokens_arc));
let data = Arc::try_unwrap(tokens_arc).unwrap_or_else(|arc| (*arc).clone());
Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
result_id: Some(result_id),
data,
})))
})
.await
}
async fn semantic_tokens_range(
&self,
params: SemanticTokensRangeParams,
) -> Result<Option<SemanticTokensRangeResult>> {
guard_async_result("semantic_tokens_range", async move {
let uri = ¶ms.text_document.uri;
let doc = match self.get_doc(uri) {
Some(d) => d,
None => {
return Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
result_id: None,
data: vec![],
})));
}
};
let tokens = semantic_tokens_range(doc.source(), &doc, params.range);
Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
result_id: None,
data: tokens,
})))
})
.await
}
async fn semantic_tokens_full_delta(
&self,
params: SemanticTokensDeltaParams,
) -> Result<Option<SemanticTokensFullDeltaResult>> {
guard_async_result("semantic_tokens_full_delta", async move {
let uri = ¶ms.text_document.uri;
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(None),
};
let new_tokens = Arc::new(semantic_tokens(doc.source(), &doc));
let new_result_id = token_hash(&new_tokens);
let prev_id = ¶ms.previous_result_id;
let result = match self.docs.get_token_cache(uri, prev_id) {
Some(old_tokens) => {
let edits = compute_token_delta(&old_tokens, &new_tokens);
SemanticTokensFullDeltaResult::TokensDelta(SemanticTokensDelta {
result_id: Some(new_result_id.clone()),
edits,
})
}
None => SemanticTokensFullDeltaResult::Tokens(SemanticTokens {
result_id: Some(new_result_id.clone()),
data: (*new_tokens).clone(),
}),
};
self.docs.store_token_cache(uri, new_result_id, new_tokens);
Ok(Some(result))
})
.await
}
async fn selection_range(
&self,
params: SelectionRangeParams,
) -> Result<Option<Vec<SelectionRange>>> {
guard_async_result("selection_range", async move {
let uri = ¶ms.text_document.uri;
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(None),
};
let ranges = selection_ranges(&doc, ¶ms.positions);
Ok(if ranges.is_empty() {
None
} else {
Some(ranges)
})
})
.await
}
async fn prepare_call_hierarchy(
&self,
params: CallHierarchyPrepareParams,
) -> Result<Option<Vec<CallHierarchyItem>>> {
guard_async_result("prepare_call_hierarchy", async move {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let source = self.get_open_text(uri).unwrap_or_default();
let word = match word_at_position(&source, position) {
Some(w) => w,
None => return Ok(None),
};
let wi = self.workspace_index_async().await;
let docs = Arc::clone(&self.docs);
let get_doc = move |u: &Url| docs.get_doc_salsa(u);
Ok(prepare_call_hierarchy_indexed(&word, &wi, &get_doc).map(|item| vec![item]))
})
.await
}
async fn incoming_calls(
&self,
params: CallHierarchyIncomingCallsParams,
) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
guard_async_result("incoming_calls", async move {
let docs = Arc::clone(&self.docs);
let item_uri = params.item.uri.to_string();
let item = params.item;
let calls = match tokio::task::spawn_blocking(move || {
let all_docs = docs.all_docs_for_scan();
incoming_calls(&item, &all_docs)
})
.await
{
Ok(c) => c,
Err(e) => {
tracing::warn!("incoming_calls panicked for {item_uri}: {e}");
vec![]
}
};
Ok(if calls.is_empty() { None } else { Some(calls) })
})
.await
}
async fn outgoing_calls(
&self,
params: CallHierarchyOutgoingCallsParams,
) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
guard_async_result("outgoing_calls", async move {
let wi = self.workspace_index_async().await;
let docs = Arc::clone(&self.docs);
let item_uri = params.item.uri.to_string();
let item = params.item;
let calls = match tokio::task::spawn_blocking(move || {
let get_doc = |u: &Url| docs.get_doc_salsa(u);
outgoing_calls_indexed(&item, &wi, &get_doc)
})
.await
{
Ok(c) => c,
Err(e) => {
tracing::warn!("outgoing_calls panicked for {item_uri}: {e}");
vec![]
}
};
Ok(if calls.is_empty() { None } else { Some(calls) })
})
.await
}
async fn document_highlight(
&self,
params: DocumentHighlightParams,
) -> Result<Option<Vec<DocumentHighlight>>> {
guard_async_result("document_highlight", async move {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let source = self.get_open_text(uri).unwrap_or_default();
let doc = match self.get_doc_stale(uri) {
Some(d) => d,
None => return Ok(None),
};
let uri_str = uri.to_string();
let highlights = match tokio::task::spawn_blocking(move || {
document_highlights(&source, &doc, position)
})
.await
{
Ok(h) => h,
Err(e) => {
tracing::warn!("document_highlight panicked for {uri_str}: {e}");
Vec::new()
}
};
Ok(if highlights.is_empty() {
None
} else {
Some(highlights)
})
})
.await
}
async fn linked_editing_range(
&self,
params: LinkedEditingRangeParams,
) -> Result<Option<LinkedEditingRanges>> {
guard_async_result("linked_editing_range", async move {
self.handle_linked_editing_range(params).await
})
.await
}
async fn goto_implementation(
&self,
params: tower_lsp::lsp_types::request::GotoImplementationParams,
) -> Result<Option<tower_lsp::lsp_types::request::GotoImplementationResponse>> {
guard_async_result("goto_implementation", async move {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let source = self.get_open_text(uri).unwrap_or_default();
let imports = self.file_imports(uri);
let raw_word = crate::text::word_at_position(&source, position).unwrap_or_default();
let wi = self.workspace_index_async().await;
let (word, mut fqn_owned): (String, Option<String>) = if raw_word.contains('\\') {
let short = raw_word
.rsplit('\\')
.next()
.unwrap_or(&raw_word)
.to_string();
let full = raw_word.trim_start_matches('\\').to_string();
(short, Some(full))
} else {
let fqn = imports.get(&raw_word).cloned();
(raw_word, fqn)
};
if fqn_owned.is_none() {
fqn_owned = wi
.classes_by_name
.get(&word)
.and_then(|refs| {
refs.iter()
.filter_map(|r| wi.at(*r))
.find(|(u, _)| *u == uri)
})
.map(|(_, cls)| cls.fqn.as_ref().to_string());
}
let fqn = fqn_owned.as_deref();
let open_docs = self.docs.docs_for(&self.open_urls());
let mut locs = find_implementations(&word, fqn, &open_docs);
if locs.is_empty() {
let fqn_for_mir = fqn.unwrap_or(&word).to_string();
let subtype_urls = self.docs.class_subtype_urls(&fqn_for_mir);
locs = find_implementations_mir_backed(&word, fqn, &wi, &subtype_urls);
}
if locs.is_empty()
&& let Some(doc) = self.get_doc(uri)
&& let Some(enclosing) =
crate::types::type_map::enclosing_class_at(&source, &doc, position)
{
let enclosing_fqn = wi
.classes_by_name
.get(&enclosing)
.and_then(|refs| refs.first())
.and_then(|r| wi.at(*r))
.map(|(_, cls)| cls.fqn.as_ref().to_string());
let method_subtype_urls = enclosing_fqn
.as_deref()
.map(|f| self.docs.class_subtype_urls(f))
.unwrap_or_default();
locs = find_method_implementations_mir_backed(
&word,
&enclosing,
&wi,
&method_subtype_urls,
);
}
if locs.is_empty() {
Ok(None)
} else {
Ok(Some(GotoDefinitionResponse::Array(locs)))
}
})
.await
}
async fn goto_declaration(
&self,
params: tower_lsp::lsp_types::request::GotoDeclarationParams,
) -> Result<Option<tower_lsp::lsp_types::request::GotoDeclarationResponse>> {
guard_async_result("goto_declaration", async move {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let source = self.get_open_text(uri).unwrap_or_default();
let open_docs = self.docs.docs_for(&self.open_urls());
if let Some(loc) = goto_declaration(&source, &open_docs, position) {
return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
}
let all_indexes = self.docs.all_indexes();
Ok(goto_declaration_from_index(&source, &all_indexes, position)
.map(GotoDefinitionResponse::Scalar))
})
.await
}
async fn goto_type_definition(
&self,
params: tower_lsp::lsp_types::request::GotoTypeDefinitionParams,
) -> Result<Option<tower_lsp::lsp_types::request::GotoTypeDefinitionResponse>> {
guard_async_result("goto_type_definition", async move {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let source = self.get_open_text(uri).unwrap_or_default();
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(None),
};
let analysis = self.cached_analysis_async(uri).await;
let open_docs = self.docs.docs_for(&self.open_urls());
let all_indexes = self.docs.all_indexes();
let mut results = goto_type_definition_exact(
&source,
&doc,
analysis.as_deref(),
&open_docs,
position,
);
if results.is_empty() {
results = goto_type_definition_from_index_exact(
&source,
&doc,
analysis.as_deref(),
&all_indexes,
position,
);
}
if results.is_empty() {
results = goto_type_definition_short_name_fallback(
&source,
&doc,
analysis.as_deref(),
&open_docs,
position,
);
}
if results.is_empty() {
results = goto_type_definition_from_index_short_name_fallback(
&source,
&doc,
analysis.as_deref(),
&all_indexes,
position,
);
}
let response = match results.len() {
0 => None,
1 => Some(GotoDefinitionResponse::Scalar(
results.into_iter().next().unwrap(),
)),
_ => Some(GotoDefinitionResponse::Array(results)),
};
Ok(response)
})
.await
}
async fn prepare_type_hierarchy(
&self,
params: TypeHierarchyPrepareParams,
) -> Result<Option<Vec<TypeHierarchyItem>>> {
guard_async_result("prepare_type_hierarchy", async move {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let source = self.get_open_text(uri).unwrap_or_default();
let wi = self.workspace_index_async().await;
Ok(
prepare_type_hierarchy_from_workspace(&source, uri, &wi, position)
.map(|item| vec![item]),
)
})
.await
}
async fn supertypes(
&self,
params: TypeHierarchySupertypesParams,
) -> Result<Option<Vec<TypeHierarchyItem>>> {
guard_async_result("supertypes", async move {
let wi = self.workspace_index_async().await;
let loaded_new = self
.ensure_direct_supertypes_loaded(¶ms.item.name, &wi)
.await;
let wi = if loaded_new {
self.workspace_index_async().await
} else {
wi
};
let result = supertypes_of_from_workspace(¶ms.item, &wi);
Ok(if result.is_empty() {
None
} else {
Some(result)
})
})
.await
}
async fn subtypes(
&self,
params: TypeHierarchySubtypesParams,
) -> Result<Option<Vec<TypeHierarchyItem>>> {
guard_async_result("subtypes", async move {
let wi = self.workspace_index_async().await;
let item_fqn = wi
.classes_by_name
.get(¶ms.item.name)
.and_then(|refs| {
refs.iter()
.filter_map(|r| wi.at(*r))
.find(|(u, _)| **u == params.item.uri)
.or_else(|| refs.first().and_then(|r| wi.at(*r)))
})
.map(|(_, cls)| cls.fqn.as_ref().to_string());
let subtype_urls = item_fqn
.as_deref()
.map(|f| self.docs.class_subtype_urls(f))
.unwrap_or_default();
let result =
subtypes_of_mir_backed(¶ms.item, item_fqn.as_deref(), &wi, &subtype_urls);
Ok(if result.is_empty() {
None
} else {
Some(result)
})
})
.await
}
async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
guard_async_result("code_lens", async move {
let uri = ¶ms.text_document.uri;
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(None),
};
let docs = Arc::clone(&self.docs);
let docs_cancel = Arc::clone(&self.docs);
let uri_owned = uri.clone();
let uri_str = uri.to_string();
let lenses = match tokio::task::spawn_blocking(move || {
let (_interactive, cancel_rev) = docs.settled_write_rev_guard();
let all_docs = docs.all_docs_for_scan();
code_lenses(&uri_owned, &doc, &all_docs, move || {
docs_cancel.write_rev() != cancel_rev
})
})
.await
{
Ok(l) => l,
Err(e) => {
tracing::warn!("code_lens panicked for {uri_str}: {e}");
vec![]
}
};
Ok(if lenses.is_empty() {
None
} else {
Some(lenses)
})
})
.await
}
async fn code_lens_resolve(&self, params: CodeLens) -> Result<CodeLens> {
let fallback = params.clone();
let resolved = guard_async("code_lens_resolve", async move {
Some(params)
})
.await;
Ok(resolved.unwrap_or(fallback))
}
async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
guard_async_result("document_link", async move {
let uri = ¶ms.text_document.uri;
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(None),
};
let links = document_links(uri, &doc, doc.source());
Ok(if links.is_empty() { None } else { Some(links) })
})
.await
}
async fn document_link_resolve(&self, params: DocumentLink) -> Result<DocumentLink> {
let fallback = params.clone();
let resolved = guard_async("document_link_resolve", async move {
Some(params)
})
.await;
Ok(resolved.unwrap_or(fallback))
}
async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
guard_async_result("formatting", async move {
let uri = ¶ms.text_document.uri;
let source = self.get_open_text(uri).unwrap_or_default();
Ok(format_document(&source))
})
.await
}
async fn range_formatting(
&self,
params: DocumentRangeFormattingParams,
) -> Result<Option<Vec<TextEdit>>> {
guard_async_result("range_formatting", async move {
let uri = ¶ms.text_document.uri;
let source = self.get_open_text(uri).unwrap_or_default();
Ok(format_range(&source, params.range))
})
.await
}
async fn on_type_formatting(
&self,
params: DocumentOnTypeFormattingParams,
) -> Result<Option<Vec<TextEdit>>> {
guard_async_result("on_type_formatting", async move {
let uri = ¶ms.text_document_position.text_document.uri;
let source = self.get_open_text(uri).unwrap_or_default();
let edits = on_type_format(
&source,
params.text_document_position.position,
¶ms.ch,
¶ms.options,
);
Ok(if edits.is_empty() { None } else { Some(edits) })
})
.await
}
async fn execute_command(
&self,
params: ExecuteCommandParams,
) -> Result<Option<serde_json::Value>> {
guard_async_result("execute_command", async move {
match params.command.as_str() {
"php-lsp.runTest" => {
let file_uri = params
.arguments
.first()
.and_then(|v| v.as_str())
.and_then(|s| Url::parse(s).ok());
let filter = params
.arguments
.get(1)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let root = self.root_paths.load().first().cloned();
let client = self.client.clone();
tokio::spawn(async move {
run_phpunit(&client, &filter, root.as_deref(), file_uri.as_ref()).await;
});
Ok(None)
}
_ => Ok(None),
}
})
.await
}
async fn will_rename_files(&self, params: RenameFilesParams) -> Result<Option<WorkspaceEdit>> {
guard_async_result("will_rename_files", async move {
self.handle_will_rename_files(params).await
})
.await
}
async fn did_rename_files(&self, params: RenameFilesParams) {
guard_async("did_rename_files", async move {
self.handle_did_rename_files(params).await
})
.await
}
async fn will_create_files(&self, params: CreateFilesParams) -> Result<Option<WorkspaceEdit>> {
guard_async_result("will_create_files", async move {
self.handle_will_create_files(params).await
})
.await
}
async fn did_create_files(&self, params: CreateFilesParams) {
guard_async("did_create_files", async move {
self.handle_did_create_files(params).await
})
.await
}
async fn will_delete_files(&self, params: DeleteFilesParams) -> Result<Option<WorkspaceEdit>> {
guard_async_result("will_delete_files", async move {
self.handle_will_delete_files(params).await
})
.await
}
async fn did_delete_files(&self, params: DeleteFilesParams) {
guard_async("did_delete_files", async move {
self.handle_did_delete_files(params).await
})
.await
}
async fn moniker(&self, params: MonikerParams) -> Result<Option<Vec<Moniker>>> {
guard_async_result("moniker", async move {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let source = self.get_open_text(uri).unwrap_or_default();
let doc = match self.get_doc(uri) {
Some(d) => d,
None => return Ok(None),
};
let imports = self.file_imports(uri);
Ok(moniker_at(&source, &doc, position, &imports).map(|m| vec![m]))
})
.await
}
async fn inline_value(&self, params: InlineValueParams) -> Result<Option<Vec<InlineValue>>> {
guard_async_result("inline_value", async move {
let uri = ¶ms.text_document.uri;
let source = self.get_open_text(uri).unwrap_or_default();
let values = inline_values_in_range(&source, params.range);
Ok(if values.is_empty() {
None
} else {
Some(values)
})
})
.await
}
async fn diagnostic(
&self,
params: DocumentDiagnosticParams,
) -> Result<DocumentDiagnosticReportResult> {
let report = guard_async("diagnostic", async move {
Some(self.handle_diagnostic(params).await)
})
.await;
match report {
Some(r) => r,
None => Ok(DocumentDiagnosticReportResult::Report(
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport::default()),
)),
}
}
async fn workspace_diagnostic(
&self,
params: WorkspaceDiagnosticParams,
) -> Result<WorkspaceDiagnosticReportResult> {
let report = guard_async("workspace_diagnostic", async move {
Some(self.handle_workspace_diagnostic(params).await)
})
.await;
match report {
Some(r) => r,
None => Ok(WorkspaceDiagnosticReportResult::Report(
WorkspaceDiagnosticReport::default(),
)),
}
}
async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
guard_async_result("code_action", async move {
self.handle_code_action(params).await
})
.await
}
async fn code_action_resolve(&self, item: CodeAction) -> Result<CodeAction> {
guard_async_result("code_action_resolve", async move {
self.handle_code_action_resolve(item).await
})
.await
}
}