use std::sync::Arc;
use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::*;
use crate::Backend;
use crate::hover::{MemberKindForOrigin, find_declaring_class, hover_for_function};
use crate::types::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CompletionItemData {
#[serde(rename = "c")]
pub class_name: String,
#[serde(rename = "m")]
pub member_name: String,
#[serde(rename = "k")]
pub kind: String,
#[serde(rename = "u")]
pub uri: String,
#[serde(rename = "e", default, skip_serializing_if = "Vec::is_empty")]
pub extra_class_names: Vec<String>,
}
fn extract_hover_markdown(hover: Hover) -> Option<String> {
match hover.contents {
HoverContents::Markup(mc) => Some(mc.value),
_ => None,
}
}
fn set_documentation(item: &mut CompletionItem, markdown: String) {
item.documentation = Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: markdown,
}));
}
impl Backend {
pub(crate) fn handle_completion_resolve(&self, mut item: CompletionItem) -> CompletionItem {
let Some(ref data_value) = item.data else {
return item;
};
let Ok(data) = serde_json::from_value::<CompletionItemData>(data_value.clone()) else {
return item;
};
let ctx = self.file_context(&data.uri);
let content = self.get_file_content(&data.uri).unwrap_or_default();
match data.kind.as_str() {
"method" | "property" | "constant" => {
self.resolve_member(&mut item, &data, &ctx, &content);
}
"function" => {
self.resolve_function(&mut item, &data, &ctx, &content);
}
"class" => {
self.resolve_class(&mut item, &data, &ctx, &content);
}
"global_constant" => {
self.resolve_global_constant(&mut item, &data);
}
_ => {}
}
item
}
fn resolve_member(
&self,
item: &mut CompletionItem,
data: &CompletionItemData,
ctx: &FileContext,
content: &str,
) {
let class_loader = self.class_loader(ctx);
let mut all_class_names = vec![data.class_name.clone()];
all_class_names.extend(data.extra_class_names.iter().cloned());
let member_kind = match data.kind.as_str() {
"method" => MemberKindForOrigin::Method,
"property" => MemberKindForOrigin::Property,
"constant" => MemberKindForOrigin::Constant,
_ => return,
};
let mut hover_markdowns: Vec<String> = Vec::new();
let mut seen_declaring: Vec<String> = Vec::new();
for class_name in &all_class_names {
let class_info: Option<Arc<ClassInfo>> = class_loader(class_name)
.or_else(|| ctx.classes.iter().find(|c| c.name == *class_name).cloned());
let Some(class_info) = class_info else {
continue;
};
let merged = crate::virtual_members::resolve_class_fully_cached(
&class_info,
&class_loader,
&self.resolved_class_cache,
);
let hover = match data.kind.as_str() {
"method" => {
let method = merged.methods.iter().find(|m| m.name == data.member_name);
method.map(|m| {
let declaring = find_declaring_class(
&merged,
&data.member_name,
&member_kind,
&class_loader,
);
(
declaring.name.clone(),
self.hover_for_method(m, &declaring, &class_loader, &data.uri, content),
)
})
}
"property" => {
let property = merged
.properties
.iter()
.find(|p| p.name == data.member_name);
property.map(|p| {
let declaring = find_declaring_class(
&merged,
&data.member_name,
&member_kind,
&class_loader,
);
(
declaring.name.clone(),
self.hover_for_property(p, &declaring, &class_loader),
)
})
}
"constant" => {
let constant = merged.constants.iter().find(|c| c.name == data.member_name);
constant.map(|c| {
let declaring = find_declaring_class(
&merged,
&data.member_name,
&member_kind,
&class_loader,
);
(
declaring.name.clone(),
self.hover_for_constant(c, &declaring, &class_loader),
)
})
}
_ => None,
};
if let Some((declaring_name, h)) = hover {
if seen_declaring.contains(&declaring_name) {
continue;
}
seen_declaring.push(declaring_name);
if let Some(md) = extract_hover_markdown(h) {
hover_markdowns.push(md);
}
}
}
if !hover_markdowns.is_empty() {
set_documentation(item, hover_markdowns.join("\n\n---\n\n"));
}
}
fn resolve_function(
&self,
item: &mut CompletionItem,
data: &CompletionItemData,
ctx: &FileContext,
content: &str,
) {
let function_loader = self.function_loader(ctx);
if let Some(func) = function_loader(&data.member_name) {
let resolved_see = self.resolve_see_refs(&func.see_refs, &data.uri, content);
let hover = hover_for_function(&func, Some(&resolved_see));
if let Some(md) = extract_hover_markdown(hover) {
set_documentation(item, md);
}
}
}
fn resolve_class(
&self,
item: &mut CompletionItem,
data: &CompletionItemData,
ctx: &FileContext,
content: &str,
) {
let class_loader = self.class_loader(ctx);
if let Some(cls) = class_loader(&data.member_name) {
let hover = self.hover_for_class_info(&cls, &data.uri, content);
if let Some(md) = extract_hover_markdown(hover) {
set_documentation(item, md);
}
}
}
fn resolve_global_constant(&self, item: &mut CompletionItem, data: &CompletionItemData) {
let name = &data.member_name;
let lookup = self.lookup_global_constant(name);
let md = match &lookup {
Some(Some(val)) => format!("```php\n<?php\nconst {} = {};\n```", name, val),
Some(None) => format!("```php\n<?php\nconst {};\n```", name),
None => return,
};
if let Some(Some(val)) = &lookup {
item.detail = Some(val.clone());
}
set_documentation(item, md);
}
}