use std::collections::BTreeMap;
use std::collections::HashMap;
use std::sync::Arc;
use glua_code_analysis::{
AsyncState, FileId, GmodRealm, InferGuard, LuaFunctionType, LuaMember, LuaMemberId,
LuaMemberKey, LuaMemberOwner, LuaOperatorId, LuaOperatorMetaMethod, LuaOperatorOwner,
LuaSemanticDeclId, LuaType, LuaTypeDecl, SemanticModel, WorkspaceId,
};
use glua_parser::{
LuaAssignStat, LuaAst, LuaAstNode, LuaCallExpr, LuaExpr, LuaFuncStat, LuaIndexExpr,
LuaIndexKey, LuaLiteralToken, LuaLocalFuncStat, LuaLocalName, LuaLocalStat, LuaStat,
LuaSyntaxId, LuaVarExpr,
};
use glua_parser::{LuaAstToken, LuaTokenKind};
use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, InlayHintLabelPart, Location};
use rowan::NodeOrToken;
use rowan::TokenAtOffset;
use tokio_util::sync::CancellationToken;
use crate::context::ClientId;
use crate::handlers::completion::get_index_alias_name;
use crate::handlers::definition::compare_function_types;
use crate::handlers::inlay_hint::build_function_hint::{build_closure_hint, build_label_parts};
pub fn build_inlay_hints(
semantic_model: &SemanticModel,
client_id: ClientId,
cancel_token: &CancellationToken,
) -> Option<Vec<InlayHint>> {
let mut result = Vec::new();
let root = semantic_model.get_root();
for node in root.clone().descendants::<LuaAst>() {
if cancel_token.is_cancelled() {
return None;
}
match node {
LuaAst::LuaClosureExpr(closure) => {
build_closure_hint(semantic_model, &mut result, closure);
}
LuaAst::LuaCallExpr(call_expr) => {
build_call_expr_param_hint(semantic_model, &mut result, call_expr.clone());
build_call_expr_await_hint(semantic_model, &mut result, call_expr.clone());
build_call_expr_meta_call_hint(semantic_model, &mut result, call_expr.clone());
build_enum_param_hint(semantic_model, &mut result, call_expr);
}
LuaAst::LuaLocalName(local_name) => {
build_local_name_hint(semantic_model, &mut result, local_name);
}
LuaAst::LuaAssignStat(assign_stat) => {
build_assign_stat_hint(semantic_model, &mut result, assign_stat);
}
LuaAst::LuaFuncStat(func_stat) => {
if !client_id.is_intellij() {
build_func_stat_override_hint(semantic_model, &mut result, func_stat.clone());
build_func_stat_closing_end_hint(semantic_model, &mut result, func_stat);
}
}
LuaAst::LuaIndexExpr(index_expr) => {
build_index_expr_hint(semantic_model, &mut result, index_expr);
}
LuaAst::LuaLocalFuncStat(local_func_stat) => {
build_local_func_stat_closing_end_hint(
semantic_model,
&mut result,
local_func_stat,
);
}
LuaAst::LuaIfStat(if_stat) => {
if semantic_model
.get_emmyrc()
.hint
.closing_end_hint_control_flow
{
build_control_flow_closing_end_hint(
semantic_model,
&mut result,
if_stat.syntax(),
"if",
);
}
}
LuaAst::LuaWhileStat(while_stat) => {
if semantic_model
.get_emmyrc()
.hint
.closing_end_hint_control_flow
{
build_control_flow_closing_end_hint(
semantic_model,
&mut result,
while_stat.syntax(),
"while",
);
}
}
LuaAst::LuaDoStat(do_stat) => {
if semantic_model
.get_emmyrc()
.hint
.closing_end_hint_control_flow
{
build_control_flow_closing_end_hint(
semantic_model,
&mut result,
do_stat.syntax(),
"do",
);
}
}
LuaAst::LuaForStat(for_stat) => {
if semantic_model
.get_emmyrc()
.hint
.closing_end_hint_control_flow
{
build_control_flow_closing_end_hint(
semantic_model,
&mut result,
for_stat.syntax(),
"for",
);
}
}
LuaAst::LuaForRangeStat(for_range_stat) => {
if semantic_model
.get_emmyrc()
.hint
.closing_end_hint_control_flow
{
build_control_flow_closing_end_hint(
semantic_model,
&mut result,
for_range_stat.syntax(),
"for",
);
}
}
_ => {}
}
}
Some(result)
}
fn build_call_expr_param_hint(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
call_expr: LuaCallExpr,
) -> Option<()> {
if !semantic_model.get_emmyrc().hint.param_hint {
return Some(());
}
let params_location = get_call_signature_param_location(semantic_model, &call_expr);
let func = semantic_model.infer_call_expr_func(call_expr.clone(), None)?;
let call_args_list = call_expr.get_args_list()?;
let colon_call = call_expr.is_colon_call();
build_call_args_for_func_type(
semantic_model,
result,
call_args_list.get_args().collect(),
colon_call,
&func,
params_location,
);
Some(())
}
fn get_call_signature_param_location(
semantic_model: &SemanticModel,
call_expr: &LuaCallExpr,
) -> Option<HashMap<String, Location>> {
let prefix_expr = call_expr.get_prefix_expr()?;
let semantic_info =
semantic_model.get_semantic_info(NodeOrToken::Node(prefix_expr.syntax().clone()))?;
let mut document = None;
let closure = if let LuaType::Signature(signature_id) = &semantic_info.typ {
let sig_file_id = signature_id.get_file_id();
let sig_position = signature_id.get_position();
document = semantic_model.get_document_by_file_id(sig_file_id);
if let Some(root) = semantic_model.get_root_by_file_id(sig_file_id) {
let token = match root.syntax().token_at_offset(sig_position) {
TokenAtOffset::Single(token) => token,
TokenAtOffset::Between(left, right) => {
if left.kind() == LuaTokenKind::TkName.into() {
left
} else {
right
}
}
TokenAtOffset::None => {
return None;
}
};
let stat = token.parent_ancestors().find_map(LuaStat::cast)?;
match stat {
LuaStat::LocalFuncStat(local_func_stat) => local_func_stat.get_closure(),
LuaStat::FuncStat(func_stat) => func_stat.get_closure(),
_ => None,
}
} else {
None
}
} else {
None
}?;
let lua_params = closure.get_params_list()?;
let document = document?;
let url = document.get_uri();
let mut lua_params_map: HashMap<String, Location> = HashMap::new();
for param in lua_params.get_params() {
if let Some(name_token) = param.get_name_token() {
let name = name_token.get_name_text().to_string();
let range = param.get_range();
let lsp_range = document.to_lsp_range(range)?;
lua_params_map.insert(name, Location::new(url.clone(), lsp_range));
} else if param.is_dots() {
let range = param.get_range();
let lsp_range = document.to_lsp_range(range)?;
lua_params_map.insert("...".to_string(), Location::new(url.clone(), lsp_range));
}
}
Some(lua_params_map)
}
fn build_call_expr_await_hint(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
call_expr: LuaCallExpr,
) -> Option<()> {
let prefix_expr = call_expr.get_prefix_expr()?;
let semantic_info =
semantic_model.get_semantic_info(NodeOrToken::Node(prefix_expr.syntax().clone()))?;
match semantic_info.typ {
LuaType::DocFunction(f) => {
if f.get_async_state() == AsyncState::Async {
let range = call_expr.get_range();
let document = semantic_model.get_document();
let lsp_range = document.to_lsp_range(range)?;
let hint = InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::String("await".to_string()),
position: lsp_range.start,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: Some(true),
data: None,
};
result.push(hint);
}
}
LuaType::Signature(signature_id) => {
let signature = semantic_model
.get_db()
.get_signature_index()
.get(&signature_id)?;
if signature.async_state == AsyncState::Async {
let range = call_expr.get_range();
let document = semantic_model.get_document();
let lsp_range = document.to_lsp_range(range)?;
let hint = InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::String("await ".to_string()),
position: lsp_range.start,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: Some(true),
data: None,
};
result.push(hint);
}
}
_ => {}
}
Some(())
}
fn build_call_args_for_func_type(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
call_args: Vec<LuaExpr>,
colon_call: bool,
func_type: &LuaFunctionType,
params_location: Option<HashMap<String, Location>>,
) -> Option<()> {
let mut params = func_type
.get_params()
.iter()
.map(|(name, _)| name.clone())
.collect::<Vec<_>>();
let colon_define = func_type.is_colon_define();
match (colon_call, colon_define) {
(false, true) => {
params.insert(0, "self".to_string());
}
(true, false) => {
if !params.is_empty() {
params.remove(0);
}
}
_ => {}
}
for (idx, name) in params.iter().enumerate() {
if idx >= call_args.len() {
break;
}
if name == "..." {
for (i, arg) in call_args.into_iter().enumerate().skip(idx) {
let label_name = format!("var{}:", i - idx);
let label = if let Some(params_location) = ¶ms_location {
if let Some(location) = params_location.get(name) {
InlayHintLabel::LabelParts(vec![InlayHintLabelPart {
value: label_name,
location: Some(location.clone()),
..Default::default()
}])
} else {
InlayHintLabel::String(label_name)
}
} else {
InlayHintLabel::String(label_name)
};
let range = arg.get_range();
let document = semantic_model.get_document();
let lsp_range = document.to_lsp_range(range)?;
let hint = InlayHint {
kind: Some(InlayHintKind::PARAMETER),
label,
position: lsp_range.start,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: Some(true),
data: None,
};
result.push(hint);
}
break;
}
let arg = &call_args[idx];
if let LuaExpr::NameExpr(name_expr) = arg
&& let Some(param_name) = name_expr.get_name_text()
&& ¶m_name == name
{
continue;
}
let document = semantic_model.get_document();
let lsp_range = document.to_lsp_range(arg.get_range())?;
let label_name = format!("{}:", name);
let label = if let Some(params_location) = ¶ms_location {
if let Some(location) = params_location.get(name) {
InlayHintLabel::LabelParts(vec![InlayHintLabelPart {
value: label_name,
location: Some(location.clone()),
..Default::default()
}])
} else {
InlayHintLabel::String(label_name)
}
} else {
InlayHintLabel::String(label_name)
};
let hint = InlayHint {
kind: Some(InlayHintKind::PARAMETER),
label,
position: lsp_range.start,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: Some(true),
data: None,
};
result.push(hint);
}
Some(())
}
fn build_local_name_hint(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
local_name: LuaLocalName,
) -> Option<()> {
let enable_local_hint = semantic_model.get_emmyrc().hint.local_hint;
let enable_vgui_inlay_hint = semantic_model.get_emmyrc().gmod.vgui.inlay_hint_enabled;
if !enable_local_hint && !enable_vgui_inlay_hint {
return Some(());
}
if let Some(parent) = local_name.syntax().parent() {
if LuaLocalFuncStat::can_cast(parent.kind().into()) {
return Some(());
}
if LuaLocalStat::can_cast(parent.kind().into()) {
let local_stat = LuaLocalStat::cast(parent)?;
let local_names = local_stat.get_local_name_list();
for (i, ln) in local_names.enumerate() {
if local_name == ln
&& let Some(value_expr) = local_stat.get_value_exprs().nth(i)
&& let LuaExpr::ClosureExpr(_) = value_expr
{
return Some(());
}
}
}
}
let typ = semantic_model
.get_semantic_info(NodeOrToken::Token(
local_name.get_name_token()?.syntax().clone(),
))?
.typ;
let vgui_panel_name = if enable_vgui_inlay_hint {
get_vgui_panel_name(semantic_model, &typ)
} else {
None
};
if !enable_local_hint && vgui_panel_name.is_none() {
return Some(());
}
if vgui_panel_name.is_none() {
match typ {
LuaType::Ref(_) | LuaType::Generic(_) | LuaType::Instance(_) => {}
_ => {
return Some(());
}
}
}
let document = semantic_model.get_document();
let range = local_name.get_range();
let lsp_range = document.to_lsp_range(range)?;
let label = if let Some((panel_name, base_name)) = vgui_panel_name {
if let Some(base_name) = base_name {
InlayHintLabel::String(format!(": VGUI Panel ({panel_name} : {base_name})"))
} else {
InlayHintLabel::String(format!(": VGUI Panel ({panel_name})"))
}
} else {
InlayHintLabel::LabelParts(build_label_parts(semantic_model, &typ))
};
let hint = InlayHint {
kind: Some(InlayHintKind::TYPE),
label,
position: lsp_range.end,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
};
result.push(hint);
Some(())
}
fn build_assign_stat_hint(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
assign_stat: LuaAssignStat,
) -> Option<()> {
if !semantic_model.get_emmyrc().gmod.vgui.inlay_hint_enabled {
return Some(());
}
let (vars, _) = assign_stat.get_var_and_expr_list();
let document = semantic_model.get_document();
for var in vars {
let Some(semantic_info) =
semantic_model.get_semantic_info(NodeOrToken::Node(var.syntax().clone()))
else {
continue;
};
let Some((panel_name, base_name)) = get_vgui_panel_name(semantic_model, &semantic_info.typ)
else {
continue;
};
let Some(lsp_range) = document.to_lsp_range(var.get_range()) else {
continue;
};
let label_string = if let Some(base_name) = base_name {
format!(": VGUI Panel ({panel_name} : {base_name})")
} else {
format!(": VGUI Panel ({panel_name})")
};
let hint = InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::String(label_string),
position: lsp_range.end,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
};
result.push(hint);
}
Some(())
}
fn get_vgui_panel_name(
semantic_model: &SemanticModel,
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 = semantic_model
.get_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 = semantic_model
.get_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(semantic_model, instance.get_base()),
LuaType::Union(union_type) => {
for union_member in union_type.into_vec() {
if let Some(panel_info) = get_vgui_panel_name(semantic_model, &union_member) {
return Some(panel_info);
}
}
None
}
_ => None,
}
}
fn build_func_stat_override_hint(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
func_stat: LuaFuncStat,
) -> Option<()> {
if !semantic_model.get_emmyrc().hint.override_hint {
return Some(());
}
let func_name = func_stat.get_func_name()?;
if let LuaVarExpr::IndexExpr(index_expr) = func_name {
let prefix_expr = index_expr.get_prefix_expr()?;
let prefix_type = semantic_model.infer_expr(prefix_expr).ok()?;
if let LuaType::Def(id) = prefix_type {
let supers = semantic_model
.get_db()
.get_type_index()
.get_super_types(&id)?;
let index_key = index_expr.get_index_key()?;
let member_key: LuaMemberKey = semantic_model.get_member_key(&index_key)?;
let guard = InferGuard::new();
for super_type in supers {
if let Some(member_id) = get_super_member_id(
semantic_model,
super_type,
&member_key,
&guard,
index_expr.get_position(),
) {
let member = semantic_model
.get_db()
.get_member_index()
.get_member(&member_id)?;
let document = semantic_model.get_document();
let last_paren_pos = func_stat
.get_closure()?
.get_params_list()?
.get_range()
.end();
let last_paren_lsp_pos = document.to_lsp_position(last_paren_pos)?;
let file_id = member.get_file_id();
let syntax_id = member.get_syntax_id();
let lsp_location =
get_override_lsp_location(semantic_model, file_id, syntax_id)?;
let hint = InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::LabelParts(vec![InlayHintLabelPart {
value: "override".to_string(),
location: Some(lsp_location),
..Default::default()
}]),
position: last_paren_lsp_pos,
text_edits: None,
tooltip: None,
padding_left: Some(true),
padding_right: None,
data: None,
};
result.push(hint);
break;
}
}
}
}
Some(())
}
pub fn get_super_member_id(
semantic_model: &SemanticModel,
super_type: LuaType,
member_key: &LuaMemberKey,
infer_guard: &InferGuard,
position_offset: rowan::TextSize,
) -> Option<LuaMemberId> {
let super_type_id = match &super_type {
LuaType::Ref(id) => id,
LuaType::Generic(generic) => generic.get_base_type_id_ref(),
_ => return None,
};
infer_guard.check(super_type_id).ok()?;
let member_infos = semantic_model.get_member_info_with_key_at_offset(
&super_type,
member_key.clone(),
true,
position_offset,
)?;
let first_property = member_infos.first()?.property_owner_id.clone()?;
if let LuaSemanticDeclId::Member(member_id) = first_property {
return Some(member_id);
}
None
}
pub fn get_override_lsp_location(
semantic_model: &SemanticModel,
file_id: FileId,
syntax_id: LuaSyntaxId,
) -> Option<lsp_types::Location> {
let document = semantic_model.get_document_by_file_id(file_id)?;
let root = semantic_model.get_root_by_file_id(file_id)?;
let node = syntax_id.to_node_from_root(root.syntax())?;
let range = if let Some(index_exor) = LuaIndexExpr::cast(node.clone()) {
index_exor.get_index_name_token()?.text_range()
} else {
node.text_range()
};
let lsp_range = document.to_lsp_location(range)?;
Some(lsp_range)
}
fn build_call_expr_meta_call_hint(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
call_expr: LuaCallExpr,
) -> Option<()> {
if !semantic_model.get_emmyrc().hint.meta_call_hint {
return Some(());
}
let prefix_expr = call_expr.get_prefix_expr()?;
let semantic_info =
semantic_model.get_semantic_info(NodeOrToken::Node(prefix_expr.syntax().clone()))?;
match &semantic_info.typ {
LuaType::Ref(id) | LuaType::Def(id) => {
let decl = semantic_model.get_db().get_type_index().get_type_decl(id)?;
if !decl.is_class() {
return Some(());
}
let call_operator_ids = get_visible_call_operator_ids(
semantic_model,
&id.clone().into(),
call_expr.get_position(),
)?;
set_meta_call_part(
semantic_model,
result,
&call_operator_ids,
call_expr,
semantic_info.typ,
)?;
}
_ => {}
}
Some(())
}
fn set_meta_call_part(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
operator_ids: &Vec<LuaOperatorId>,
call_expr: LuaCallExpr,
target_type: LuaType,
) -> Option<()> {
let (operator_id, call_func) =
find_match_meta_call_operator_id(semantic_model, operator_ids, call_expr.clone())?;
let operator = semantic_model
.get_db()
.get_operator_index()
.get_operator(&operator_id)?;
let location = {
let range = operator.get_range();
let document = semantic_model.get_document_by_file_id(operator.get_file_id())?;
let lsp_range = document.to_lsp_range(range)?;
Location::new(document.get_uri(), lsp_range)
};
let document = semantic_model.get_document();
let parent = call_expr.syntax().parent()?;
let hint_new = {
LuaStat::can_cast(parent.kind().into())
&& !matches!(call_expr.get_prefix_expr()?, LuaExpr::CallExpr(_))
&& semantic_model
.type_check(call_func.get_ret(), &target_type)
.is_ok()
};
let (value, hint_range, padding_right) = if hint_new {
("new".to_string(), call_expr.get_range(), Some(true))
} else {
(
":call".to_string(),
call_expr.get_prefix_expr()?.get_range(),
None,
)
};
let hint_position = {
let lsp_range = document.to_lsp_range(hint_range)?;
if hint_new {
lsp_range.start
} else {
lsp_range.end
}
};
let part = InlayHintLabelPart {
value,
location: Some(location),
..Default::default()
};
let hint = InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::LabelParts(vec![part]),
position: hint_position,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right,
data: None,
};
result.push(hint);
Some(())
}
fn find_match_meta_call_operator_id(
semantic_model: &SemanticModel,
operator_ids: &Vec<LuaOperatorId>,
call_expr: LuaCallExpr,
) -> Option<(LuaOperatorId, Arc<LuaFunctionType>)> {
let call_func = semantic_model.infer_call_expr_func(call_expr.clone(), None)?;
if operator_ids.len() == 1 {
return Some((operator_ids.first().cloned()?, call_func));
}
for operator_id in operator_ids {
let operator = semantic_model
.get_db()
.get_operator_index()
.get_operator(operator_id)?;
let operator_func = {
let operator_type = operator.get_operator_func(semantic_model.get_db());
match operator_type {
LuaType::DocFunction(func) => func,
LuaType::Signature(signature_id) => {
let signature = semantic_model
.get_db()
.get_signature_index()
.get(&signature_id)?;
signature.to_doc_func_type()
}
_ => return None,
}
};
let is_match =
compare_function_types(semantic_model, &call_func, &operator_func, &call_expr)
.unwrap_or(false);
if is_match {
return Some((*operator_id, operator_func));
}
}
operator_ids.first().cloned().map(|id| (id, call_func))
}
fn get_visible_call_operator_ids(
semantic_model: &SemanticModel,
owner: &LuaOperatorOwner,
caller_position: rowan::TextSize,
) -> Option<Vec<LuaOperatorId>> {
let operator_ids = semantic_model
.get_db()
.get_operator_index()
.get_operators(owner, LuaOperatorMetaMethod::Call)?;
let module_index = semantic_model.get_db().get_module_index();
let caller_file_id = semantic_model.get_file_id();
let caller_workspace_id = module_index.get_workspace_id(caller_file_id);
let mut priority_tiers = BTreeMap::new();
for operator_id in operator_ids {
let priority = match caller_workspace_id {
Some(caller_workspace_id) => {
let candidate_workspace_id = module_index
.get_workspace_id(operator_id.file_id)
.unwrap_or(WorkspaceId::MAIN);
let Some(priority) = module_index
.workspace_resolution_priority(caller_workspace_id, candidate_workspace_id)
else {
continue;
};
priority
}
None => 0,
};
priority_tiers
.entry(priority)
.or_insert_with(Vec::new)
.push(*operator_id);
}
let priority_tiers: Vec<(u8, Vec<LuaOperatorId>)> = priority_tiers.into_iter().collect();
let fallback_operator_ids = priority_tiers
.first()
.map(|(_, operator_ids)| operator_ids.clone())
.unwrap_or_default();
if !semantic_model.get_emmyrc().gmod.enabled {
return Some(fallback_operator_ids);
}
let infer_index = semantic_model.get_db().get_gmod_infer_index();
let caller_realm = infer_index.get_realm_at_offset(&caller_file_id, caller_position);
for (_, tier_operator_ids) in priority_tiers {
let compatible_operator_ids = tier_operator_ids
.into_iter()
.filter(|operator_id| {
let operator_realm =
infer_index.get_realm_at_offset(&operator_id.file_id, operator_id.position);
is_operator_realm_compatible(caller_realm, operator_realm)
})
.collect::<Vec<_>>();
if !compatible_operator_ids.is_empty() {
return Some(compatible_operator_ids);
}
}
Some(fallback_operator_ids)
}
fn is_operator_realm_compatible(caller_realm: GmodRealm, candidate_realm: GmodRealm) -> bool {
!matches!(
(caller_realm, candidate_realm),
(GmodRealm::Client, GmodRealm::Server) | (GmodRealm::Server, GmodRealm::Client)
)
}
fn build_index_expr_hint(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
index_expr: LuaIndexExpr,
) -> Option<()> {
if !semantic_model.get_emmyrc().hint.index_hint {
return Some(());
}
let index_key = index_expr.get_index_key()?;
if !matches!(index_key, LuaIndexKey::Integer(_)) {
return Some(());
}
let prefix_expr = index_expr.get_prefix_expr()?;
let prefix_type = semantic_model.infer_expr(prefix_expr).ok()?;
let member_key = semantic_model.get_member_key(&index_key)?;
let member_infos = semantic_model.get_member_info_with_key_at_offset(
&prefix_type,
member_key,
false,
index_expr.syntax().text_range().start(),
)?;
let member_info = member_infos.first()?;
let alias = get_index_alias_name(semantic_model, member_info)?;
let document = semantic_model.get_document();
let position = {
let index_token = index_expr.get_index_name_token()?;
let range = index_token.text_range();
let lsp_range = document.to_lsp_range(range)?;
lsp_range.end
};
let label_location = {
let range = index_expr.get_index_key()?.get_range()?;
let lsp_range = document.to_lsp_range(range)?;
Location::new(document.get_uri(), lsp_range)
};
let hint = InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::LabelParts(vec![InlayHintLabelPart {
value: format!(": {}", alias),
location: Some(label_location),
..Default::default()
}]),
position,
text_edits: None,
tooltip: None,
padding_left: Some(true),
padding_right: None,
data: None,
};
result.push(hint);
Some(())
}
fn build_enum_param_hint(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
call_expr: LuaCallExpr,
) -> Option<()> {
if !semantic_model.get_emmyrc().hint.enum_param_hint {
return Some(());
}
let func_type = semantic_model.infer_call_expr_func(call_expr.clone(), None)?;
let call_args = call_expr.get_args_list()?.get_args().collect::<Vec<_>>();
let params = func_type.get_params();
let colon_call = call_expr.is_colon_call();
let colon_define = func_type.is_colon_define();
let param_offset: i32 = match (colon_call, colon_define) {
(true, false) => 1,
(false, true) => -1,
_ => 0,
};
for (i, arg) in call_args.iter().enumerate() {
let param_index = i as i32 + param_offset;
if param_index < 0 {
continue;
}
process_enum_hint_for_arg(semantic_model, result, arg, params, param_index as usize);
}
Some(())
}
fn process_enum_hint_for_arg(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
arg: &LuaExpr,
params: &[(String, Option<LuaType>)],
param_index: usize,
) -> Option<()> {
let (_, param_type) = params.get(param_index)?;
let param_type = param_type.as_ref()?;
let type_id = match param_type {
LuaType::Ref(id) => id,
_ => return None,
};
let type_decl = semantic_model
.get_db()
.get_type_index()
.get_type_decl(type_id)?;
if !type_decl.is_enum() {
return None;
}
let arg_type = semantic_model.infer_expr(arg.clone()).ok()?;
let member_decl = find_matching_enum_member(semantic_model, type_decl, &arg_type)?;
let member_name = member_decl.get_key().to_path();
match arg {
LuaExpr::LiteralExpr(literal_expr) => {
if let Some(literal_token) = literal_expr.get_literal() {
match literal_token {
LuaLiteralToken::String(string_token) => {
if string_token.get_value() == member_name {
return None;
}
}
LuaLiteralToken::Number(number_token) => {
if number_token.is_int() {
let number_value = format!("[{}]", number_token.get_number_value());
if number_value == member_name {
return None;
}
}
}
_ => {}
}
}
}
LuaExpr::NameExpr(name_expr) => {
if let Some(arg_name) = name_expr.get_name_text() {
if member_name == arg_name {
return None;
}
let lower_arg_name = arg_name.to_lowercase();
let lower_enum_name = type_decl.get_name().to_lowercase();
let lower_member_name = member_name.to_lowercase();
if lower_arg_name.contains(&lower_enum_name)
&& lower_arg_name.contains(&lower_member_name)
{
return None;
}
}
}
LuaExpr::IndexExpr(index_expr) => {
if let Some(index_name_token) = index_expr.get_index_name_token()
&& let Some(name_token) = glua_parser::LuaNameToken::cast(index_name_token.clone())
{
let index_name = name_token.get_name_text();
if index_name == member_name {
return None;
}
}
}
_ => {}
}
let enum_name = type_decl.get_name();
let hint_text = format!("{}.{}", enum_name, member_name);
let document = semantic_model.get_document();
let range = arg.get_range();
let lsp_range = document.to_lsp_range(range)?;
let hint = InlayHint {
kind: Some(InlayHintKind::PARAMETER),
label: InlayHintLabel::String(hint_text),
position: lsp_range.end,
text_edits: None,
tooltip: None,
padding_left: Some(true),
padding_right: None,
data: None,
};
result.push(hint);
Some(())
}
fn find_matching_enum_member<'a>(
semantic_model: &'a SemanticModel,
type_decl: &LuaTypeDecl,
arg_type: &LuaType,
) -> Option<&'a LuaMember> {
let enum_member_owner = LuaMemberOwner::Type(type_decl.get_id());
let enum_members = semantic_model
.get_db()
.get_member_index()
.get_members(&enum_member_owner)?;
let is_enum_key = type_decl.is_enum_key();
for member_decl in enum_members {
let is_match = if is_enum_key {
let member_key = member_decl.get_key();
match (member_key, arg_type) {
(LuaMemberKey::Name(s), LuaType::StringConst(arg_s)) => s == arg_s.as_ref(),
(LuaMemberKey::Integer(i), LuaType::IntegerConst(arg_i)) => *i == *arg_i,
(LuaMemberKey::ExprType(typ), _) => typ == arg_type,
_ => false,
}
} else if let Some(type_cache) = semantic_model
.get_db()
.get_type_index()
.get_type_cache(&member_decl.get_id().into())
{
type_cache.as_type() == arg_type
} else {
false
};
if is_match {
return Some(member_decl);
}
}
None
}
fn exceeds_min_lines(
semantic_model: &SemanticModel,
start_offset: rowan::TextSize,
end_offset: rowan::TextSize,
) -> bool {
let document = semantic_model.get_document();
let min_lines = semantic_model.get_emmyrc().hint.closing_end_hint_min_lines;
let Some(start_line) = document.get_line(start_offset) else {
return false;
};
let Some(end_line) = document.get_line(end_offset) else {
return false;
};
end_line.saturating_sub(start_line) >= min_lines as usize
}
fn get_scripted_entity_suffix(
semantic_model: &SemanticModel,
member_owner: &LuaMemberOwner,
) -> Option<String> {
let type_id = match member_owner {
LuaMemberOwner::Type(type_id) => type_id,
_ => return None,
};
let type_name = type_id.get_simple_name();
if let Some(base_name) = semantic_model
.get_db()
.get_gmod_class_metadata_index()
.get_vgui_panel_base(type_name)
{
return Some(match base_name {
Some(base) => format!(" ({type_name} : {base})"),
None => format!(" ({type_name})"),
});
}
let supers = semantic_model
.get_db()
.get_type_index()
.get_super_types(type_id)?;
for super_type in supers {
let super_id = match &super_type {
LuaType::Ref(id) => id,
_ => continue,
};
let super_name = super_id.get_simple_name();
match super_name {
"ENT" | "Entity" | "SWEP" | "Weapon" | "EFFECT" | "TOOL" | "Tool" | "PLUGIN" | "GM" => {
return Some(format!(" ({type_name} : {super_name})"));
}
_ => {}
}
}
None
}
fn build_func_stat_closing_end_hint(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
func_stat: LuaFuncStat,
) -> Option<()> {
if !semantic_model.get_emmyrc().hint.closing_end_hint {
return Some(());
}
let closure = func_stat.get_closure()?;
let end_token = closure.token_by_kind(LuaTokenKind::TkEnd)?;
let stat_range = func_stat.get_range();
if !exceeds_min_lines(
semantic_model,
stat_range.start(),
end_token.get_range().end(),
) {
return Some(());
}
let document = semantic_model.get_document();
let end_pos = document.to_lsp_position(end_token.get_range().end())?;
let func_name = func_stat.get_func_name()?;
let (label_text, location) = match &func_name {
LuaVarExpr::IndexExpr(index_expr) => {
let index_name_token = index_expr.get_index_name_token()?;
let name_range = document.to_lsp_range(index_name_token.text_range())?;
let prefix_text = index_expr
.get_prefix_expr()
.map(|p| p.syntax().text().to_string());
let separator = if index_expr.get_index_name_token().is_some() {
index_expr
.syntax()
.children_with_tokens()
.filter_map(|it| it.into_token())
.find(|t| {
t.kind() == LuaTokenKind::TkColon.into()
|| t.kind() == LuaTokenKind::TkDot.into()
})
.map(|t| t.text().to_string())
.unwrap_or_else(|| ".".to_string())
} else {
".".to_string()
};
let method_name = index_name_token.text().to_string();
let full_name = match prefix_text {
Some(prefix) => format!("{prefix}{separator}{method_name}"),
None => method_name,
};
let location = Location::new(document.get_uri(), name_range);
let file_id = semantic_model.get_file_id();
let member_id = LuaMemberId::new(index_expr.get_syntax_id(), file_id);
let suffix = semantic_model
.get_db()
.get_member_index()
.get_member_owner(&member_id)
.and_then(|owner| get_scripted_entity_suffix(semantic_model, owner))
.unwrap_or_default();
(format!("{full_name}{suffix}"), location)
}
LuaVarExpr::NameExpr(name_expr) => {
let name_token = name_expr.get_name_token()?;
let name_range = document.to_lsp_range(name_token.get_range())?;
let name_text = name_token.get_name_text().to_string();
let location = Location::new(document.get_uri(), name_range);
(name_text, location)
}
};
let hint = InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::LabelParts(vec![InlayHintLabelPart {
value: label_text,
location: Some(location),
..Default::default()
}]),
position: end_pos,
text_edits: None,
tooltip: None,
padding_left: Some(true),
padding_right: None,
data: None,
};
result.push(hint);
Some(())
}
fn build_local_func_stat_closing_end_hint(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
local_func_stat: LuaLocalFuncStat,
) -> Option<()> {
if !semantic_model.get_emmyrc().hint.closing_end_hint {
return Some(());
}
let closure = local_func_stat.get_closure()?;
let end_token = closure.token_by_kind(LuaTokenKind::TkEnd)?;
let stat_range = local_func_stat.get_range();
if !exceeds_min_lines(
semantic_model,
stat_range.start(),
end_token.get_range().end(),
) {
return Some(());
}
let document = semantic_model.get_document();
let end_pos = document.to_lsp_position(end_token.get_range().end())?;
let local_name = local_func_stat.get_local_name()?;
let name_token = local_name.get_name_token()?;
let name_text = name_token.get_name_text().to_string();
let name_range = document.to_lsp_range(name_token.get_range())?;
let location = Location::new(document.get_uri(), name_range);
let hint = InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::LabelParts(vec![InlayHintLabelPart {
value: name_text.to_string(),
location: Some(location),
..Default::default()
}]),
position: end_pos,
text_edits: None,
tooltip: None,
padding_left: Some(true),
padding_right: None,
data: None,
};
result.push(hint);
Some(())
}
fn build_control_flow_closing_end_hint(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
stat_syntax: &glua_parser::LuaSyntaxNode,
keyword: &str,
) -> Option<()> {
if !semantic_model.get_emmyrc().hint.closing_end_hint {
return Some(());
}
let end_token = stat_syntax
.children_with_tokens()
.filter_map(|it| it.into_token())
.filter(|t| t.kind() == LuaTokenKind::TkEnd.into())
.last()?;
let end_range = end_token.text_range();
if !exceeds_min_lines(
semantic_model,
stat_syntax.text_range().start(),
end_range.end(),
) {
return Some(());
}
let document = semantic_model.get_document();
let end_pos = document.to_lsp_position(end_range.end())?;
let hint = InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::String(keyword.to_string()),
position: end_pos,
text_edits: None,
tooltip: None,
padding_left: Some(true),
padding_right: None,
data: None,
};
result.push(hint);
Some(())
}