use std::collections::HashSet;
use glua_code_analysis::{
DbIndex, LuaCompilation, LuaDeclExtra, LuaDeclId, LuaDocument, LuaMemberId, LuaMemberKey,
LuaMemberOwner, LuaSemanticDeclId, LuaSignatureId, LuaType, LuaTypeDeclId, RenderLevel,
SemanticDeclLevel, SemanticInfo, SemanticModel,
};
use glua_code_analysis::{humanize_member_key_name, humanize_type};
use glua_parser::{
LuaAssignStat, LuaAstNode, LuaCallArgList, LuaExpr, LuaFuncStat, LuaIndexExpr, LuaSyntaxKind,
LuaSyntaxToken, LuaTableExpr, LuaTableField, LuaVarExpr, PathTrait,
};
use lsp_types::{Hover, HoverContents, MarkedString, MarkupContent};
use rowan::{TextRange, TextSize};
use crate::handlers::completion::{color_info_from_expr, color_info_from_type, is_color_type};
use crate::handlers::hover::function::{build_function_hover, is_function};
use crate::handlers::hover::humanize_type_decl::build_type_decl_hover;
use crate::handlers::hover::humanize_types::hover_humanize_type;
use super::{
color_swatch::color_swatch_markdown,
find_origin::{find_decl_origin_owners, find_member_origin_owners},
hover_builder::HoverBuilder,
humanize_types::hover_const_type,
};
pub fn build_semantic_info_hover(
compilation: &LuaCompilation,
semantic_model: &SemanticModel,
db: &DbIndex,
document: &LuaDocument,
token: LuaSyntaxToken,
semantic_info: SemanticInfo,
range: TextRange,
render_level: Option<RenderLevel>,
) -> Option<Hover> {
let typ = semantic_info.clone().typ;
if semantic_info.semantic_decl.is_none() {
return build_hover_without_property(db, semantic_model, document, token, typ);
}
let hover_builder = build_hover_content(
compilation,
semantic_model,
db,
Some(typ),
semantic_info.semantic_decl.unwrap(),
false,
Some(token.clone()),
render_level,
);
if let Some(hover_builder) = hover_builder {
hover_builder.build_hover_result(document.to_lsp_range(range))
} else {
None
}
}
pub fn build_assignment_target_hover(
compilation: &LuaCompilation,
semantic_model: &SemanticModel,
db: &DbIndex,
document: &LuaDocument,
token: LuaSyntaxToken,
render_level: Option<RenderLevel>,
) -> Option<Hover> {
let typ = get_assignment_target_type(&token, semantic_model)?;
let range = token.text_range();
if let Some(semantic_decl) = get_assignment_target_semantic_decl(db, semantic_model, &token) {
let hover_builder = build_hover_content(
compilation,
semantic_model,
db,
Some(typ),
semantic_decl,
false,
Some(token.clone()),
render_level,
)?;
return hover_builder.build_hover_result(document.to_lsp_range(range));
}
build_hover_without_property(db, semantic_model, document, token, typ)
}
fn build_hover_without_property(
db: &DbIndex,
semantic_model: &SemanticModel,
document: &LuaDocument,
token: LuaSyntaxToken,
typ: LuaType,
) -> Option<Hover> {
if let Some(hover) =
build_dynamic_field_hover_without_property(db, semantic_model, &token, &typ)
{
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: lsp_types::MarkupKind::Markdown,
value: hover,
}),
range: document.to_lsp_range(token.text_range()),
});
}
let render_level = RenderLevel::Detailed;
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()),
})
}
fn build_dynamic_field_hover_without_property(
db: &DbIndex,
semantic_model: &SemanticModel,
token: &LuaSyntaxToken,
typ: &LuaType,
) -> Option<String> {
let index_expr = token.parent()?.ancestors().find_map(LuaIndexExpr::cast)?;
let index_key = index_expr.get_index_key()?;
let key_range = index_key.get_range()?;
if !key_range.contains_range(token.text_range()) {
return None;
}
let field_name = index_key.get_path_part();
if field_name.is_empty() {
return None;
}
let display_field_name = humanize_member_key_name(&field_name);
let prefix_type = semantic_model
.infer_expr(index_expr.get_prefix_expr()?)
.ok()?;
if !is_dynamic_field_for_type(
db,
semantic_model,
&prefix_type,
&field_name,
index_expr.get_position(),
) {
return None;
}
let hover_type = if let Some(assignment_type) =
prefer_concrete_assignment_type_for_token(token, semantic_model, typ)
{
assignment_type
} else if matches!(typ, LuaType::Nil | LuaType::Unknown) {
LuaType::Any
} else {
typ.clone()
};
let type_humanize_text = if hover_type.is_const() {
hover_const_type(db, &hover_type, RenderLevel::Detailed)
} else {
humanize_type(db, &hover_type, RenderLevel::Simple)
};
Some(format!(
"```lua\n(infer) {}: {}\n```",
display_field_name, type_humanize_text
))
}
fn is_dynamic_field_for_type(
db: &DbIndex,
semantic_model: &SemanticModel,
typ: &LuaType,
field_name: &str,
position_offset: TextSize,
) -> bool {
let emmyrc = db.get_emmyrc();
if !emmyrc.gmod.enabled || !emmyrc.gmod.infer_dynamic_fields {
return false;
}
let key = LuaMemberKey::Name(field_name.into());
semantic_model
.get_member_info_with_key_at_offset(typ, key, true, position_offset)
.is_some_and(|members| {
members
.iter()
.any(|member| member.property_owner_id.is_none())
})
}
pub fn build_hover_content_for_completion<'a>(
compilation: &'a LuaCompilation,
semantic_model: &'a SemanticModel,
db: &DbIndex,
property_id: LuaSemanticDeclId,
) -> 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(
compilation,
semantic_model,
db,
typ,
property_id,
true,
None,
None,
)
}
fn build_hover_content<'a>(
compilation: &'a LuaCompilation,
semantic_model: &'a SemanticModel,
db: &DbIndex,
typ: Option<LuaType>,
property_id: LuaSemanticDeclId,
is_completion: bool,
token: Option<LuaSyntaxToken>,
render_level: Option<RenderLevel>,
) -> Option<HoverBuilder<'a>> {
let mut builder = match render_level {
Some(level) => HoverBuilder::new_with_level(
compilation,
semantic_model,
token,
is_completion,
Some(level),
),
None => HoverBuilder::new(compilation, 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 mut semantic_decls =
find_decl_origin_owners(builder.compilation, builder.semantic_model, decl_id)
.get_types(builder.semantic_model);
if is_function(&typ) {
adjust_semantic_decls(
builder,
&mut semantic_decls,
&LuaSemanticDeclId::LuaDecl(decl_id),
&typ,
);
build_function_hover(builder, db, &semantic_decls);
if let Some((LuaSemanticDeclId::Member(member_id), _)) = semantic_decls
.iter()
.find(|(decl, _)| matches!(decl, LuaSemanticDeclId::Member(_)))
{
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 {
if typ.is_const() {
let const_value = hover_const_type(db, &typ, builder.detail_render_level);
let prefix = if decl.is_local() {
"local "
} else {
"(global) "
};
builder.set_type_description(format!("{}{}: {}", prefix, decl.get_name(), const_value));
} else {
let decl_hover_type =
get_hover_type(builder, builder.semantic_model).unwrap_or(typ.clone());
let type_humanize_text =
hover_humanize_type(builder, &decl_hover_type, Some(builder.detail_render_level));
let prefix = if decl.is_local() {
"local "
} else {
"(global) "
};
builder.set_type_description(format!(
"{}{}: {}",
prefix,
decl.get_name(),
type_humanize_text
));
}
let mut semantic_decl_set = HashSet::new();
let decl_decl = LuaSemanticDeclId::LuaDecl(decl_id);
semantic_decl_set.insert(&decl_decl);
if !is_completion {
semantic_decl_set.extend(semantic_decls.iter().map(|(decl, _)| decl));
}
for semantic_decl in semantic_decl_set {
builder.add_description(semantic_decl);
}
}
if let Some(desc) = get_gmod_class_description(db, &typ) {
builder.add_annotation_description(desc);
}
if !is_completion {
add_decl_color_preview(builder, db, &typ, decl_id);
}
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.compilation,
builder.semantic_model,
member_id,
true,
builder
.get_trigger_token()
.map(|token| token.text_range().start()),
)
.get_types(builder.semantic_model);
let should_render_as_function = is_function(&typ)
|| semantic_decls
.iter()
.any(|(_, semantic_typ)| is_function(semantic_typ));
let member_name = match member.get_key() {
LuaMemberKey::Name(name) => humanize_member_key_name(name.as_str()),
LuaMemberKey::Integer(i) => format!("[{}]", i),
_ => return None,
};
if should_render_as_function {
extend_gmod_hook_semantic_decls(builder, db, member, &mut semantic_decls);
adjust_semantic_decls(
builder,
&mut semantic_decls,
&LuaSemanticDeclId::Member(member_id),
&typ,
);
build_function_hover(builder, db, &semantic_decls);
builder.set_location_path(Some(member));
let mut has_signature_docs = false;
let current_signature_type = builder.semantic_model.get_type(member.get_id().into());
let before_len = builder.annotation_description.len();
builder.add_signature_params_rets_description(current_signature_type);
if builder.annotation_description.len() > before_len {
has_signature_docs = true;
}
if !has_signature_docs {
for (_, signature_type) in &semantic_decls {
let before_len = builder.annotation_description.len();
builder.add_signature_params_rets_description(signature_type.clone());
if builder.annotation_description.len() > before_len {
break;
}
}
}
} else {
if typ.is_const() {
let const_value = hover_const_type(db, &typ, builder.detail_render_level);
builder.set_type_description(format!("(field) {}: {}", member_name, const_value));
builder.set_location_path(Some(member));
} else {
let unique_origin_types: Vec<LuaType> = {
let mut seen: Vec<LuaType> = Vec::new();
for (_, t) in &semantic_decls {
if !seen.iter().any(|s| s == t) {
seen.push(t.clone());
}
}
seen
};
let member_hover_type = if unique_origin_types.len() > 1 {
let mut acc = unique_origin_types[0].clone();
for t in &unique_origin_types[1..] {
acc = glua_code_analysis::TypeOps::Union.apply(db, &acc, t);
}
prefer_concrete_assignment_type_for_builder(builder, builder.semantic_model, &acc)
.unwrap_or(acc)
} else if typ.is_union() {
prefer_concrete_assignment_type_for_builder(builder, builder.semantic_model, &typ)
.unwrap_or(typ.clone())
} else {
prefer_concrete_assignment_type_for_builder(builder, builder.semantic_model, &typ)
.or_else(|| get_hover_type(builder, builder.semantic_model))
.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));
builder
.set_type_description(format!("(field) {}: {}", member_name, type_humanize_text));
builder.set_location_path(Some(member));
}
let mut semantic_decl_set = HashSet::new();
let member_decl = LuaSemanticDeclId::Member(member.get_id());
semantic_decl_set.insert(&member_decl);
if !is_completion {
semantic_decl_set.extend(semantic_decls.iter().map(|(decl, _)| decl));
}
for semantic_decl in semantic_decl_set {
builder.add_description(semantic_decl);
}
}
if let Some(desc) = get_gmod_class_description(db, &typ) {
builder.add_annotation_description(desc);
}
if !is_completion {
add_member_color_preview(builder, db, &typ, member_id);
}
Some(())
}
fn add_decl_color_preview(
builder: &mut HoverBuilder,
db: &DbIndex,
typ: &LuaType,
decl_id: LuaDeclId,
) -> Option<()> {
if !db.get_emmyrc().document_color.enable {
return None;
}
let color = color_info_from_type(typ).or_else(|| {
if !is_color_type(typ) && !matches!(typ, LuaType::Unknown) {
return None;
}
let decl = db.get_decl_index().get_decl(&decl_id)?;
let value_syntax_id = decl.get_value_syntax_id()?;
if !can_hover_value_expr_be_color(value_syntax_id.get_kind()) {
return None;
}
let tree = db.get_vfs().get_syntax_tree(&decl_id.file_id)?;
let value_node = value_syntax_id.to_node_from_root(&tree.get_red_root())?;
LuaExpr::cast(value_node).and_then(|expr| color_info_from_expr(&expr))
})?;
let decl = db.get_decl_index().get_decl(&decl_id)?;
let prefix = if decl.is_local() {
"local "
} else {
"(global) "
};
builder.set_type_description(format!(
"{}{}: {}",
prefix,
decl.get_name(),
color.gmod_display
));
builder.add_annotation_description(color_swatch_markdown(
color.red,
color.green,
color.blue,
color.alpha,
&color.gmod_display,
));
Some(())
}
fn add_member_color_preview(
builder: &mut HoverBuilder,
db: &DbIndex,
typ: &LuaType,
member_id: LuaMemberId,
) -> Option<()> {
if !db.get_emmyrc().document_color.enable {
return None;
}
let color = color_info_from_type(typ).or_else(|| {
if !is_color_type(typ) && !matches!(typ, LuaType::Unknown) {
return None;
}
get_member_value_expr(db, member_id).and_then(|expr| color_info_from_expr(&expr))
})?;
let member = db.get_member_index().get_member(&member_id)?;
let member_name = match member.get_key() {
LuaMemberKey::Name(name) => humanize_member_key_name(name.as_str()),
_ => return None,
};
builder.set_type_description(format!("(field) {}: {}", member_name, color.gmod_display));
builder.add_annotation_description(color_swatch_markdown(
color.red,
color.green,
color.blue,
color.alpha,
&color.gmod_display,
));
Some(())
}
fn can_hover_value_expr_be_color(kind: LuaSyntaxKind) -> bool {
matches!(kind, LuaSyntaxKind::CallExpr | LuaSyntaxKind::LiteralExpr)
}
fn get_member_value_expr(db: &DbIndex, member_id: LuaMemberId) -> Option<LuaExpr> {
let root = db
.get_vfs()
.get_syntax_tree(&member_id.file_id)?
.get_red_root();
let node = member_id.get_syntax_id().to_node_from_root(&root)?;
if let Some(field) = LuaTableField::cast(node.clone()) {
return field.get_value_expr();
}
if let Some(index_expr) = LuaIndexExpr::cast(node) {
if let Some(assign_stat) = index_expr.get_parent::<LuaAssignStat>() {
let (vars, value_exprs) = assign_stat.get_var_and_expr_list();
let value_idx = vars
.iter()
.position(|var| var.get_syntax_id() == index_expr.get_syntax_id())?;
return value_exprs.get(value_idx).cloned();
}
if let Some(func_stat) = index_expr.get_parent::<LuaFuncStat>() {
return func_stat.get_closure().map(LuaExpr::ClosureExpr);
}
}
None
}
fn get_gmod_class_description(db: &DbIndex, typ: &LuaType) -> Option<String> {
if !db.get_emmyrc().gmod.enabled {
return None;
}
if let Some((panel_name, base_name)) = get_vgui_panel_name(db, typ) {
return Some(match base_name {
Some(base) => format!("---\n**VGUI Panel:** `{panel_name}` (Base: `{base}`)"),
None => format!("---\n**VGUI Panel:** `{panel_name}`"),
});
}
let type_id = match typ {
LuaType::Ref(id) | LuaType::Def(id) => id,
_ => return None,
};
let supers = db.get_type_index().get_super_types(type_id)?;
for super_type in supers {
let super_name = match &super_type {
LuaType::Def(id) | LuaType::Ref(id) => id.get_simple_name(),
_ => continue,
};
let label = match super_name {
"Entity" => "Scripted Entity",
"Weapon" => "Scripted Weapon",
"CEffect" => "Scripted Effect",
"Tool" => "Tool",
"Plugin" => "Plugin",
"Gamemode" => "Gamemode",
_ => continue,
};
return Some(format!(
"---\n**{label}:** `{}` (Base: `{super_name}`)",
type_id.get_simple_name()
));
}
None
}
fn get_vgui_panel_name(db: &DbIndex, typ: &LuaType) -> Option<(String, Option<String>)> {
match typ {
LuaType::Ref(type_decl_id) | LuaType::Def(type_decl_id) => {
let type_name = type_decl_id.get_simple_name();
let base_name = db
.get_gmod_class_metadata_index()
.get_vgui_panel_base(type_name)?;
Some((type_name.to_string(), base_name))
}
LuaType::Generic(generic) => {
let type_decl_id = generic.get_base_type_id_ref();
let type_name = type_decl_id.get_simple_name();
let base_name = db
.get_gmod_class_metadata_index()
.get_vgui_panel_base(type_name)?;
Some((type_name.to_string(), base_name))
}
LuaType::Instance(instance) => get_vgui_panel_name(db, instance.get_base()),
LuaType::Union(union_type) => {
for union_member in union_type.into_vec() {
if let Some(panel_info) = get_vgui_panel_name(db, &union_member) {
return Some(panel_info);
}
}
None
}
_ => None,
}
}
fn extend_gmod_hook_semantic_decls(
builder: &HoverBuilder,
db: &DbIndex,
member: &glua_code_analysis::LuaMember,
semantic_decls: &mut Vec<(LuaSemanticDeclId, LuaType)>,
) {
if !builder.semantic_model.get_emmyrc().gmod.enabled {
return;
}
let Some(LuaMemberOwner::Type(owner_type_decl_id)) =
db.get_member_index().get_current_owner(&member.get_id())
else {
return;
};
let fallback_owner_names = gmod_hook_owner_fallbacks(owner_type_decl_id.get_simple_name());
if fallback_owner_names.is_empty() {
return;
}
let member_key = member.get_key().clone();
let trigger_position = builder
.get_trigger_token()
.map(|token| token.text_range().start());
extend_gmod_hook_same_owner_semantic_decls(
builder,
db,
owner_type_decl_id,
&member_key,
trigger_position,
semantic_decls,
);
for fallback_owner_name in fallback_owner_names {
let fallback_type = LuaType::Ref(LuaTypeDeclId::global(fallback_owner_name));
let member_infos = match trigger_position {
Some(trigger_position) => builder.semantic_model.get_member_info_with_key_at_offset(
&fallback_type,
member_key.clone(),
true,
trigger_position,
),
None => builder.semantic_model.get_member_info_with_key(
&fallback_type,
member_key.clone(),
true,
),
};
let Some(member_infos) = member_infos else {
continue;
};
for member_info in member_infos {
let Some(property_owner_id) = member_info.property_owner_id else {
continue;
};
if semantic_decls
.iter()
.any(|(decl_id, _)| decl_id == &property_owner_id)
{
continue;
}
let owner_type = match property_owner_id {
LuaSemanticDeclId::LuaDecl(decl_id) => {
builder.semantic_model.get_type(decl_id.into())
}
LuaSemanticDeclId::Member(member_id) => {
builder.semantic_model.get_type(member_id.into())
}
_ => continue,
};
if !is_function(&owner_type) {
continue;
}
semantic_decls.push((property_owner_id, owner_type));
}
}
}
fn extend_gmod_hook_same_owner_semantic_decls(
builder: &HoverBuilder,
db: &DbIndex,
owner_type_decl_id: &LuaTypeDeclId,
member_key: &LuaMemberKey,
trigger_position: Option<rowan::TextSize>,
semantic_decls: &mut Vec<(LuaSemanticDeclId, LuaType)>,
) {
let LuaMemberKey::Name(hook_name) = member_key else {
return;
};
let module_index = db.get_module_index();
let caller_workspace_id = module_index.get_workspace_id(builder.semantic_model.get_file_id());
let caller_realm = trigger_position.map_or_else(
|| {
db.get_gmod_infer_index()
.get_realm_file_metadata(&builder.semantic_model.get_file_id())
.map(|metadata| metadata.inferred_realm)
.unwrap_or(glua_code_analysis::GmodRealm::Unknown)
},
|trigger_position| {
db.get_gmod_infer_index()
.get_realm_at_offset(&builder.semantic_model.get_file_id(), trigger_position)
},
);
let target_access_path = format!("{}.{}", owner_type_decl_id.get_simple_name(), hook_name);
let mut same_owner_candidates = Vec::new();
for file_id in db.get_vfs().get_all_file_ids() {
let workspace_priority = if let Some(caller_workspace_id) = caller_workspace_id {
let Some(candidate_workspace_id) = module_index.get_workspace_id(file_id) else {
continue;
};
let Some(priority) = module_index
.workspace_resolution_priority(caller_workspace_id, candidate_workspace_id)
else {
continue;
};
priority
} else {
0
};
let Some(semantic_model) = builder.compilation.get_semantic_model(file_id) else {
continue;
};
let Some(tree) = semantic_model.get_db().get_vfs().get_syntax_tree(&file_id) else {
continue;
};
let root = tree.get_red_root();
for func_stat in tree.get_chunk_node().descendants::<LuaFuncStat>() {
let Some(func_name) = func_stat.get_func_name() else {
continue;
};
if func_name.get_access_path().as_deref() != Some(target_access_path.as_str()) {
continue;
}
let Some(node) = func_stat.get_syntax_id().to_node_from_root(&root) else {
continue;
};
let Some(func_stat) = LuaFuncStat::cast(node) else {
continue;
};
let Some(func_name) = func_stat.get_func_name() else {
continue;
};
let Some(LuaSemanticDeclId::Member(member_id)) = semantic_model.find_decl(
func_name.syntax().clone().into(),
SemanticDeclLevel::default(),
) else {
continue;
};
let Some(LuaMemberOwner::Type(candidate_owner)) =
db.get_member_index().get_current_owner(&member_id)
else {
continue;
};
if *candidate_owner != *owner_type_decl_id {
continue;
}
let candidate_realm = db
.get_gmod_infer_index()
.get_realm_at_offset(&member_id.file_id, member_id.get_position());
if !super::is_realm_compatible(caller_realm, candidate_realm) {
continue;
}
let same_owner_decl = LuaSemanticDeclId::Member(member_id);
if semantic_decls
.iter()
.any(|(decl_id, _)| decl_id == &same_owner_decl)
{
continue;
}
let owner_type = semantic_model.get_type(member_id.into());
if !is_function(&owner_type) {
continue;
}
same_owner_candidates.push((workspace_priority, same_owner_decl, owner_type));
}
}
let Some(best_priority) = same_owner_candidates
.iter()
.map(|(priority, _, _)| *priority)
.min()
else {
return;
};
for (_, same_owner_decl, owner_type) in same_owner_candidates
.into_iter()
.filter(|(priority, _, _)| *priority == best_priority)
{
semantic_decls.push((same_owner_decl, owner_type));
}
}
fn gmod_hook_owner_fallbacks(owner_name: &str) -> &'static [&'static str] {
if owner_name.eq_ignore_ascii_case("GM") || owner_name.eq_ignore_ascii_case("GAMEMODE") {
&["SANDBOX"]
} else if owner_name.eq_ignore_ascii_case("PLUGIN") {
&["GM", "GAMEMODE", "SANDBOX"]
} else if owner_name.eq_ignore_ascii_case("SANDBOX") {
&["GM", "GAMEMODE"]
} else {
&[]
}
}
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
));
}
}
if !s.is_empty() {
marked_strings.push(MarkedString::from_markdown(s));
}
Some(())
}
pub fn get_hover_type(builder: &HoverBuilder, semantic_model: &SemanticModel) -> Option<LuaType> {
let trigger_token = builder.get_trigger_token()?;
get_assignment_target_type(&trigger_token, semantic_model)
}
fn get_assignment_target_type(
trigger_token: &LuaSyntaxToken,
semantic_model: &SemanticModel,
) -> Option<LuaType> {
let mut ancestor = trigger_token.parent();
let assign_stat = loop {
let node = ancestor?;
if let Some(assign_stat) = LuaAssignStat::cast(node.clone()) {
break assign_stat;
}
ancestor = node.parent();
};
let (vars, exprs) = assign_stat.get_var_and_expr_list();
for (i, var) in vars.iter().enumerate() {
if token_is_assignment_target(var, trigger_token) {
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) => {
return muli_return.get_type(multi_return_index).cloned();
}
_ => return Some(expr_type),
},
Err(_) => return None,
}
}
}
None
}
fn get_assignment_target_semantic_decl(
db: &DbIndex,
semantic_model: &SemanticModel,
trigger_token: &LuaSyntaxToken,
) -> Option<LuaSemanticDeclId> {
let file_id = semantic_model.get_file_id();
let mut ancestor = trigger_token.parent();
let assign_stat = loop {
let node = ancestor?;
if let Some(assign_stat) = LuaAssignStat::cast(node.clone()) {
break assign_stat;
}
ancestor = node.parent();
};
let (vars, _) = assign_stat.get_var_and_expr_list();
for var in vars {
if !token_is_assignment_target(&var, trigger_token) {
continue;
}
return match var {
LuaVarExpr::NameExpr(name_expr) => {
let decl_id = db
.get_reference_index()
.get_var_reference_decl(&file_id, name_expr.get_range())?;
Some(LuaSemanticDeclId::LuaDecl(decl_id))
}
LuaVarExpr::IndexExpr(index_expr) => Some(LuaSemanticDeclId::Member(LuaMemberId::new(
index_expr.get_syntax_id(),
file_id,
))),
};
}
None
}
fn token_is_assignment_target(var: &LuaVarExpr, trigger_token: &LuaSyntaxToken) -> bool {
match var {
LuaVarExpr::NameExpr(name_expr) => name_expr
.syntax()
.text_range()
.contains(trigger_token.text_range().start()),
LuaVarExpr::IndexExpr(index_expr) => index_expr
.get_index_key()
.and_then(|key| key.get_range())
.is_some_and(|range| range.contains_range(trigger_token.text_range())),
}
}
fn prefer_concrete_assignment_type_for_builder(
builder: &HoverBuilder,
semantic_model: &SemanticModel,
current_type: &LuaType,
) -> Option<LuaType> {
let trigger_token = builder.get_trigger_token()?;
prefer_concrete_assignment_type_for_token(&trigger_token, semantic_model, current_type)
}
fn prefer_concrete_assignment_type_for_token(
trigger_token: &LuaSyntaxToken,
semantic_model: &SemanticModel,
current_type: &LuaType,
) -> Option<LuaType> {
if !contains_open_table_any(current_type) {
return None;
}
let assignment_type = get_assignment_target_type(trigger_token, semantic_model)?;
if is_specific_assignment_type(&assignment_type) {
Some(assignment_type)
} else {
None
}
}
fn contains_open_table_any(typ: &LuaType) -> bool {
match typ {
LuaType::Any => true,
LuaType::Union(union) => union
.into_vec()
.iter()
.any(|typ| matches!(typ, LuaType::Any)),
_ => false,
}
}
fn is_specific_assignment_type(typ: &LuaType) -> bool {
!matches!(
typ,
LuaType::Any | LuaType::Unknown | LuaType::Nil | LuaType::Never
)
}
#[allow(unused)]
fn adjust_semantic_decls(
builder: &mut HoverBuilder,
semantic_decls: &mut Vec<(LuaSemanticDeclId, LuaType)>,
current_semantic_decl_id: &LuaSemanticDeclId,
current_type: &LuaType,
) -> Option<()> {
if let Some(pos) = semantic_decls
.iter()
.position(|(decl, typ)| decl == current_semantic_decl_id && current_type == typ)
{
let item = semantic_decls.remove(pos);
semantic_decls.push(item);
return Some(());
}
if let Some(pos) = semantic_decls
.iter()
.position(|(_, typ)| current_type == typ)
{
let should_preserve_current_member = matches!(
current_semantic_decl_id,
LuaSemanticDeclId::Member(member_id)
if builder
.semantic_model
.get_db()
.get_member_index()
.get_member(member_id)
.is_some_and(|member| {
member.get_syntax_id().get_kind() == LuaSyntaxKind::IndexExpr
})
) && semantic_decls
.get(pos)
.is_some_and(|(decl, _)| decl != current_semantic_decl_id);
if should_preserve_current_member
&& has_add_to_semantic_decls(builder, current_semantic_decl_id).unwrap_or(true)
{
semantic_decls.push((current_semantic_decl_id.clone(), current_type.clone()));
} else {
let item = semantic_decls.remove(pos);
semantic_decls.push(item);
}
return Some(());
}
let current_len = semantic_decls.len();
if current_len == 0 {
semantic_decls.push((current_semantic_decl_id.clone(), current_type.clone()));
return Some(());
}
if let LuaSemanticDeclId::LuaDecl(_) = current_semantic_decl_id {
return Some(());
}
if semantic_decls.iter().any(|(decl, typ)| {
decl == current_semantic_decl_id && !typ.is_signature() && !typ.is_unknown()
}) {
return Some(());
}
if has_add_to_semantic_decls(builder, current_semantic_decl_id).unwrap_or(true) {
semantic_decls.push((current_semantic_decl_id.clone(), current_type.clone()));
};
Some(())
}
fn has_add_to_semantic_decls(
builder: &mut HoverBuilder,
semantic_decl_id: &LuaSemanticDeclId,
) -> Option<bool> {
if let LuaSemanticDeclId::Member(member_id) = semantic_decl_id {
let semantic_model = if member_id.file_id == builder.semantic_model.get_file_id() {
builder.semantic_model
} else {
&builder.compilation.get_semantic_model(member_id.file_id)?
};
let root = semantic_model.get_root().syntax();
let current_node = member_id.get_syntax_id().to_node_from_root(root)?;
if member_id.get_syntax_id().get_kind() == LuaSyntaxKind::TableFieldAssign {
if LuaTableField::can_cast(current_node.kind().into()) {
let table_field = LuaTableField::cast(current_node.clone())?;
let parent = table_field.syntax().parent()?;
let table_expr = LuaTableExpr::cast(parent)?;
let table_type = semantic_model.infer_table_should_be(table_expr.clone())?;
if matches!(table_type, LuaType::Ref(_) | LuaType::Generic(_)) {
let is_in_call = table_expr.ancestors::<LuaCallArgList>().next().is_some();
return Some(!is_in_call);
}
}
};
}
Some(true)
}