use emmylua_code_analysis::humanize_type;
use emmylua_code_analysis::{
DbIndex, LuaDeclExtra, LuaDeclId, LuaDocument, LuaMemberId, LuaMemberKey, LuaSemanticDeclId,
LuaSignatureId, LuaType, LuaTypeDeclId, RenderLevel, SemanticInfo, SemanticModel,
};
use emmylua_parser::{LuaAssignStat, LuaAstNode, LuaExpr, LuaSyntaxToken};
use lsp_types::{Hover, HoverContents, MarkedString, MarkupContent};
use rowan::TextRange;
use crate::handlers::common::{find_decl_origin_owners, find_member_origin_owners};
use crate::handlers::hover::function::{build_function_hover, has_function_candidate, is_function};
use crate::handlers::hover::humanize_type_decl::build_type_decl_hover;
use crate::handlers::hover::humanize_types::{
DescriptionInfo, HoverTypeRenderContext, extract_description_from_property_owner,
hover_humanize_type,
};
use super::{
HoverDeclContext, HoverDeclInfo, hover_builder::HoverBuilder, humanize_types::hover_const_type,
};
pub fn build_semantic_info_hover(
semantic_model: &SemanticModel,
db: &DbIndex,
document: &LuaDocument,
token: LuaSyntaxToken,
semantic_info: SemanticInfo,
range: TextRange,
) -> Option<Hover> {
let typ = semantic_info.clone().typ;
if semantic_info.semantic_decl.is_none() {
return build_hover_without_property(db, document, token, typ);
}
let hover_builder = build_hover_content(
semantic_model,
db,
Some(typ),
semantic_info.semantic_decl.unwrap(),
false,
Some(token.clone()),
);
if let Some(hover_builder) = hover_builder {
hover_builder.build_hover_result(document.to_lsp_range(range))
} else {
None
}
}
fn build_hover_without_property(
db: &DbIndex,
document: &LuaDocument,
token: LuaSyntaxToken,
typ: LuaType,
) -> Option<Hover> {
let render_level = db
.get_emmyrc()
.hover
.custom_detail
.map_or(RenderLevel::Detailed, |custom_detail| {
RenderLevel::CustomDetailed(custom_detail)
});
let hover = humanize_type(db, &typ, render_level);
Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: lsp_types::MarkupKind::Markdown,
value: hover,
}),
range: document.to_lsp_range(token.text_range()),
})
}
pub fn build_hover_content_for_completion<'a>(
semantic_model: &'a SemanticModel,
db: &DbIndex,
property_id: LuaSemanticDeclId,
token: Option<LuaSyntaxToken>,
) -> Option<HoverBuilder<'a>> {
let typ = match property_id {
LuaSemanticDeclId::LuaDecl(decl_id) => {
Some(semantic_model.get_type(decl_id.into()).clone())
}
LuaSemanticDeclId::Member(member_id) => {
Some(semantic_model.get_type(member_id.into()).clone())
}
_ => None,
};
build_hover_content(semantic_model, db, typ, property_id, true, token)
}
fn build_hover_content<'a>(
semantic_model: &'a SemanticModel,
db: &DbIndex,
typ: Option<LuaType>,
property_id: LuaSemanticDeclId,
is_completion: bool,
token: Option<LuaSyntaxToken>,
) -> Option<HoverBuilder<'a>> {
let mut builder = HoverBuilder::new(semantic_model, token, is_completion);
match property_id {
LuaSemanticDeclId::LuaDecl(decl_id) => {
let typ = typ?;
build_decl_hover(&mut builder, db, typ, decl_id, is_completion)?;
}
LuaSemanticDeclId::Member(member_id) => {
let typ = typ?;
build_member_hover(&mut builder, db, typ, member_id, is_completion);
}
LuaSemanticDeclId::TypeDecl(type_decl_id) => {
build_type_decl_hover(&mut builder, db, type_decl_id);
}
_ => return None,
}
Some(builder)
}
fn build_decl_hover(
builder: &mut HoverBuilder,
db: &DbIndex,
typ: LuaType,
decl_id: LuaDeclId,
is_completion: bool,
) -> Option<()> {
let decl = db.get_decl_index().get_decl(&decl_id)?;
let semantic_decls =
find_decl_origin_owners(builder.semantic_model, decl_id).get_types(builder.semantic_model);
if is_function(&typ) {
let origin_decls = into_hover_decl_infos(semantic_decls);
let hover_decl_context = HoverDeclContext::new(
HoverDeclInfo::new(LuaSemanticDeclId::LuaDecl(decl_id), typ.clone()),
origin_decls,
);
build_function_hover(builder, db, &hover_decl_context);
if let Some(decl_info) = hover_decl_context
.origin_decls()
.iter()
.find(|decl_info| matches!(decl_info.id(), LuaSemanticDeclId::Member(_)))
&& let LuaSemanticDeclId::Member(member_id) = decl_info.id()
{
let member = db.get_member_index().get_member(member_id);
builder.set_location_path(member);
}
builder
.add_signature_params_rets_description(builder.semantic_model.get_type(decl_id.into()));
} else {
let target_type = builder.semantic_model.get_type(decl_id.into()).clone();
if typ.is_const() {
let const_value = hover_const_type(db, &typ);
let prefix = if decl.is_param() {
"(parameter) "
} else if decl.is_local() {
"local "
} else {
"(global) "
};
builder.set_type_description(format!("{}{}: {}", prefix, decl.get_name(), const_value));
} else {
let decl_hover_type =
get_assignment_hover_type(builder, builder.semantic_model, &target_type, &typ)
.unwrap_or(typ.clone());
let type_humanize_text = hover_humanize_type(
builder,
&decl_hover_type,
Some(builder.detail_render_level),
HoverTypeRenderContext::SymbolHover,
);
let prefix = if decl.is_param() {
"(parameter) "
} else if decl.is_local() {
"local "
} else {
"(global) "
};
builder.set_type_description(format!(
"{}{}: {}",
prefix,
decl.get_name(),
type_humanize_text
));
}
add_hover_descriptions(
builder,
LuaSemanticDeclId::LuaDecl(decl_id),
&target_type,
semantic_decls.iter().map(|(decl, typ)| (decl, typ)),
is_completion,
);
}
if let LuaDeclExtra::Param {
idx, signature_id, ..
} = &decl.extra
{
if let Some(signature) = db.get_signature_index().get(signature_id)
&& let Some(param_info) = signature.get_param_info_by_id(*idx)
&& let Some(description) = ¶m_info.description
{
builder.add_annotation_description(description.clone());
}
}
Some(())
}
fn build_member_hover(
builder: &mut HoverBuilder,
db: &DbIndex,
typ: LuaType,
member_id: LuaMemberId,
is_completion: bool,
) -> Option<()> {
let member = db.get_member_index().get_member(&member_id)?;
let mut semantic_decls = find_member_origin_owners(builder.semantic_model, member_id, true)
.get_types(builder.semantic_model);
if let Some(token) = builder.get_trigger_token() {
semantic_decls.retain(|(semantic_decl, _)| {
builder
.semantic_model
.is_semantic_visible(token.clone(), semantic_decl.clone())
});
}
let member_name = match member.get_key() {
LuaMemberKey::Name(name) => name.to_string(),
LuaMemberKey::Integer(i) => format!("[{}]", i),
_ => return None,
};
let origin_decls = into_hover_decl_infos(semantic_decls);
let hover_decl_context = HoverDeclContext::new(
HoverDeclInfo::new(LuaSemanticDeclId::Member(member_id), typ.clone()),
origin_decls,
);
if has_function_candidate(&hover_decl_context) {
build_function_hover(builder, db, &hover_decl_context);
builder.set_location_path(Some(member));
builder.add_signature_params_rets_description(
builder.semantic_model.get_type(member.get_id().into()),
);
} else {
let target_type = builder
.semantic_model
.get_type(member.get_id().into())
.clone();
if typ.is_const() {
let const_value = hover_const_type(db, &typ);
builder.set_type_description(format!("(field) {}: {}", member_name, const_value));
builder.set_location_path(Some(member));
} else {
let member_hover_type =
get_assignment_hover_type(builder, builder.semantic_model, &target_type, &typ)
.unwrap_or(typ.clone());
let level = if member_hover_type.is_module_ref() {
builder.detail_render_level
} else {
RenderLevel::Simple
};
let type_humanize_text = hover_humanize_type(
builder,
&member_hover_type,
Some(level),
HoverTypeRenderContext::SymbolHover,
);
builder
.set_type_description(format!("(field) {}: {}", member_name, type_humanize_text));
builder.set_location_path(Some(member));
}
add_hover_descriptions(
builder,
LuaSemanticDeclId::Member(member.get_id()),
&target_type,
hover_decl_context
.origin_decls()
.iter()
.map(|decl_info| (decl_info.id(), decl_info.typ())),
is_completion,
);
}
Some(())
}
fn add_hover_descriptions<'a, I>(
builder: &mut HoverBuilder,
primary_owner: LuaSemanticDeclId,
target_type: &LuaType,
origin_decls: I,
is_completion: bool,
) where
I: IntoIterator<Item = (&'a LuaSemanticDeclId, &'a LuaType)>,
{
let mut description_owners = Vec::new();
description_owners.push(primary_owner);
collect_type_decl_description_owners(target_type, &mut description_owners);
if !is_completion {
for (origin_owner, origin_type) in origin_decls {
if !description_owners.contains(origin_owner) {
description_owners.push(origin_owner.clone());
}
collect_type_decl_description_owners(origin_type, &mut description_owners);
}
}
let mut seen_descriptions: Vec<DescriptionInfo> = Vec::new();
for owner in &description_owners {
if let Some(desc_info) =
extract_description_from_property_owner(builder.semantic_model, owner)
{
if seen_descriptions.iter().any(|seen| {
seen.description == desc_info.description
&& seen.tag_content == desc_info.tag_content
}) {
continue;
}
seen_descriptions.push(desc_info.clone());
builder.add_description_from_info(Some(desc_info));
}
}
}
fn collect_type_decl_description_owners(
typ: &LuaType,
description_owners: &mut Vec<LuaSemanticDeclId>,
) {
match typ {
LuaType::Def(type_decl_id) | LuaType::Ref(type_decl_id) => {
push_type_decl_description_owner(description_owners, type_decl_id.clone());
}
LuaType::Generic(generic) => {
push_type_decl_description_owner(description_owners, generic.get_base_type_id());
}
LuaType::Instance(instance) => {
collect_type_decl_description_owners(instance.get_base(), description_owners);
}
LuaType::Union(union) => {
for typ in union.into_vec() {
collect_type_decl_description_owners(&typ, description_owners);
}
}
LuaType::Intersection(intersection) => {
for typ in intersection.get_types() {
collect_type_decl_description_owners(typ, description_owners);
}
}
_ => {}
}
}
fn push_type_decl_description_owner(
description_owners: &mut Vec<LuaSemanticDeclId>,
type_decl_id: LuaTypeDeclId,
) {
let owner = LuaSemanticDeclId::TypeDecl(type_decl_id);
if !description_owners.contains(&owner) {
description_owners.push(owner);
}
}
fn into_hover_decl_infos(semantic_decls: Vec<(LuaSemanticDeclId, LuaType)>) -> Vec<HoverDeclInfo> {
semantic_decls
.into_iter()
.map(|(semantic_decl_id, typ)| HoverDeclInfo::new(semantic_decl_id, typ))
.collect()
}
pub fn add_signature_param_description(
db: &DbIndex,
marked_strings: &mut Vec<MarkedString>,
signature_id: LuaSignatureId,
) -> Option<()> {
let signature = db.get_signature_index().get(&signature_id)?;
let param_count = signature.params.len();
let mut s = String::new();
for i in 0..param_count {
let param_info = match signature.get_param_info_by_id(i) {
Some(info) => info,
None => continue,
};
if let Some(description) = ¶m_info.description {
s.push_str(&format!(
"@*param* `{}` — {}\n\n",
param_info.name, description
));
}
}
if !s.is_empty() {
marked_strings.push(MarkedString::from_markdown(s));
}
Some(())
}
pub fn add_signature_ret_description(
db: &DbIndex,
marked_strings: &mut Vec<MarkedString>,
signature_id: LuaSignatureId,
) -> Option<()> {
let signature = db.get_signature_index().get(&signature_id)?;
let mut s = String::new();
for i in 0..signature.return_docs.len() {
let ret_info = &signature.return_docs[i];
if let Some(description) = ret_info.description.clone() {
s.push_str(&format!(
"@*return* {} — {}\n\n",
match &ret_info.name {
Some(name) if !name.is_empty() => format!("`{}` ", name),
_ => "".to_string(),
},
description
));
}
}
for ret_overload in &signature.return_overloads {
let return_overload_types = ret_overload
.type_refs
.iter()
.map(|ty| humanize_type(db, ty, RenderLevel::Simple))
.collect::<Vec<_>>()
.join(", ");
let description = ret_overload.description.as_deref().unwrap_or_default();
if description.is_empty() {
s.push_str(&format!(
"@*return_overload* `{}`\n\n",
return_overload_types
));
} else {
s.push_str(&format!(
"@*return_overload* `{}` — {}\n\n",
return_overload_types, description
));
}
}
if !s.is_empty() {
marked_strings.push(MarkedString::from_markdown(s));
}
Some(())
}
fn get_assignment_hover_type(
builder: &HoverBuilder,
semantic_model: &SemanticModel,
target_type: &LuaType,
fallback_type: &LuaType,
) -> Option<LuaType> {
let assign_stat = LuaAssignStat::cast(builder.get_trigger_token()?.parent()?.parent()?)?;
let (vars, exprs) = assign_stat.get_var_and_expr_list();
for (i, var) in vars.iter().enumerate() {
if var
.syntax()
.text_range()
.contains(builder.get_trigger_token()?.text_range().start())
{
let mut expr: Option<&LuaExpr> = exprs.get(i);
let multi_return_index = if expr.is_none() {
expr = Some(exprs.last()?);
i + 1 - exprs.len()
} else {
0
};
let expr_type = semantic_model.infer_expr(expr.unwrap().clone());
match expr_type {
Ok(expr_type) => match expr_type {
LuaType::Variadic(muli_return) => {
let expr_type = muli_return.get_type(multi_return_index).cloned()?;
return select_assignment_hover_type(
semantic_model,
target_type,
fallback_type,
expr_type,
);
}
_ => {
return select_assignment_hover_type(
semantic_model,
target_type,
fallback_type,
expr_type,
);
}
},
Err(_) => return None,
}
}
}
None
}
fn select_assignment_hover_type(
semantic_model: &SemanticModel,
target_type: &LuaType,
fallback_type: &LuaType,
expr_type: LuaType,
) -> Option<LuaType> {
let mut should_keep = false;
if matches!(expr_type, LuaType::Table | LuaType::TableConst(_)) {
let mut type_decl_description_owners = Vec::new();
collect_type_decl_description_owners(target_type, &mut type_decl_description_owners);
should_keep = !type_decl_description_owners.is_empty();
}
if should_keep {
return Some(target_type.clone());
}
if semantic_model.type_check(target_type, &expr_type).is_ok() {
return Some(expr_type);
}
Some(fallback_type.clone())
}