use glua_code_analysis::{
DbIndex, LuaAliasCallKind, LuaMemberId, LuaMemberInfo, LuaMemberKey, LuaMemberOwner,
LuaSemanticDeclId, LuaType, SemanticModel, get_keyof_members,
try_extract_signature_id_from_field,
};
use glua_parser::{
LuaAssignStat, LuaAstNode, LuaAstToken, LuaExpr, LuaFuncStat, LuaGeneralToken, LuaIndexExpr,
LuaParenExpr, LuaTableField, LuaTokenKind,
};
use lsp_types::CompletionItem;
use crate::handlers::completion::{
add_completions::get_function_snippet,
completion_builder::CompletionBuilder,
completion_data::{CompletionColorInfo, CompletionData},
providers::{apply_staged_call_snippet, get_function_remove_nil},
};
use super::{
CallDisplay, check_visibility, color_label_detail,
completion_item_info::{
color_info_from_expr, color_info_from_type, gmod_constructor_literal_detail, is_color_type,
is_gmod_literal_constructor_type, scalar_literal_description, scalar_literal_detail,
},
get_completion_kind, get_completion_tags, get_description, get_detail, is_deprecated,
is_table_namespace_type,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CompletionTriggerStatus {
Dot,
Colon,
InString,
LeftBracket,
}
pub fn add_member_completion(
builder: &mut CompletionBuilder,
member_info: LuaMemberInfo,
status: CompletionTriggerStatus,
overload_count: Option<usize>,
) -> Option<()> {
add_member_completion_with_description_hint(builder, member_info, status, overload_count, None)
}
pub fn add_member_completion_with_description_hint(
builder: &mut CompletionBuilder,
member_info: LuaMemberInfo,
status: CompletionTriggerStatus,
overload_count: Option<usize>,
description_hint: Option<&str>,
) -> Option<()> {
if builder.is_cancelled() {
return None;
}
let property_owner = &member_info.property_owner_id;
if let Some(property_owner) = &property_owner {
check_visibility(builder, property_owner.clone())?;
}
let member_key = &member_info.key;
let mut can_add_snippet = true;
let label = match status {
CompletionTriggerStatus::Dot => match member_key {
LuaMemberKey::Name(name) => name.to_string(),
LuaMemberKey::Integer(index) => format!("[{}]", index),
LuaMemberKey::ExprType(typ) => {
if let LuaType::Call(alias_call) = typ {
if alias_call.get_call_kind() == LuaAliasCallKind::KeyOf
&& alias_call.get_operands().len() == 1
{
let members = get_keyof_members(
builder.semantic_model.get_db(),
&alias_call.get_operands()[0],
)
.unwrap_or_default();
let member_keys = members.iter().map(|m| m.key.clone()).collect::<Vec<_>>();
for key in member_keys {
let mut member_info = member_info.clone();
member_info.key = key;
add_member_completion(builder, member_info, status, None);
}
}
}
return None;
}
_ => return None,
},
CompletionTriggerStatus::Colon => match member_key {
LuaMemberKey::Name(name) => name.to_string(),
_ => return None,
},
CompletionTriggerStatus::InString => {
can_add_snippet = false;
match member_key {
LuaMemberKey::Name(name) => name.to_string(),
_ => return None,
}
}
CompletionTriggerStatus::LeftBracket => {
can_add_snippet = false;
match member_key {
LuaMemberKey::Name(name) => format!("\"{}\"", name),
LuaMemberKey::Integer(index) => format!("{}", index),
_ => return None,
}
}
};
let typ = &member_info.typ;
let remove_nil_type =
get_function_remove_nil(builder.semantic_model.get_db(), typ).unwrap_or(typ.clone());
if status == CompletionTriggerStatus::Colon && !is_completion_callable_type(&remove_nil_type) {
return None;
}
let (color, constructor_literal_detail) =
get_member_completion_literal_info(builder, property_owner, &remove_nil_type);
let completion_data_color = builder
.semantic_model
.get_emmyrc()
.document_color
.enable
.then(|| color.clone())
.flatten();
let completion_data = if let Some(id) = &property_owner {
if let Some(index) = member_info.overload_index {
CompletionData::from_overload(builder, id.clone(), index, overload_count)
} else if let Some(color) = completion_data_color {
CompletionData::from_property_owner_id_with_color(
builder,
id.clone(),
overload_count,
color,
)
} else {
CompletionData::from_property_owner_id(builder, id.clone(), overload_count)
}
} else {
None
};
let call_display = get_call_show(builder.semantic_model.get_db(), &remove_nil_type, status)
.unwrap_or(CallDisplay::None);
let is_inferred_dynamic_member = property_owner.is_none();
let literal_detail = scalar_literal_detail(&remove_nil_type);
let detail = if let Some(color) = &color {
Some(color_label_detail(color))
} else if is_inferred_dynamic_member {
None
} else {
get_detail(builder, &remove_nil_type, call_display)
.or(constructor_literal_detail)
.or(literal_detail)
};
let description = if color.is_some() {
Some("Color".to_string())
} else if is_inferred_dynamic_member {
None
} else {
scalar_literal_description(&remove_nil_type)
.or_else(|| get_description(builder, &remove_nil_type))
};
let description = apply_description_hint(description, description_hint);
let deprecated = property_owner
.as_ref()
.map(|id| is_deprecated(builder, id.clone()));
let tags = get_completion_tags(builder, deprecated);
let kind = if color.is_some() {
lsp_types::CompletionItemKind::COLOR
} else if is_inferred_dynamic_member {
lsp_types::CompletionItemKind::VARIABLE
} else {
get_member_completion_kind(
builder.semantic_model.get_db(),
property_owner,
&remove_nil_type,
status,
)
};
let mut completion_item = CompletionItem {
label: label.clone(),
kind: Some(kind),
data: completion_data,
label_details: Some(lsp_types::CompletionItemLabelDetails {
detail,
description,
}),
deprecated,
tags,
..Default::default()
};
if status == CompletionTriggerStatus::Dot
&& member_key.is_integer()
&& builder.trigger_token.kind() == LuaTokenKind::TkDot.into()
{
let document = builder.semantic_model.get_document();
let remove_range = builder.trigger_token.text_range();
let lsp_remove_range = document.to_lsp_range(remove_range)?;
completion_item.additional_text_edits = Some(vec![lsp_types::TextEdit {
range: lsp_remove_range,
new_text: "".to_string(),
}]);
}
if matches!(
status,
CompletionTriggerStatus::Dot | CompletionTriggerStatus::Colon
) && (builder.trigger_token.kind() == LuaTokenKind::TkDot.into()
|| builder.trigger_token.kind() == LuaTokenKind::TkColon.into())
{
resolve_function_params(
builder,
&mut completion_item,
&remove_nil_type,
call_display,
);
}
if can_add_snippet {
if apply_staged_call_snippet(builder, &label, status, &mut completion_item).is_none()
&& builder.support_snippets(typ)
&& let Some(snippet) = get_function_snippet(builder, &label, typ, call_display)
{
completion_item.insert_text = Some(snippet);
completion_item.insert_text_format = Some(lsp_types::InsertTextFormat::SNIPPET);
}
}
if !try_add_alias_completion_item_new(builder, &member_info, &completion_item, &label)
.unwrap_or(false)
{
builder.add_completion_item(completion_item)?;
}
add_signature_overloads(
builder,
property_owner,
&remove_nil_type,
call_display,
deprecated,
Some(kind),
label,
overload_count,
description_hint,
);
Some(())
}
fn apply_description_hint(
description: Option<String>,
description_hint: Option<&str>,
) -> Option<String> {
let Some(hint) = description_hint else {
return description;
};
Some(match description {
Some(description) => format!("{hint} - {description}"),
None => hint.to_string(),
})
}
fn get_member_completion_literal_info(
builder: &CompletionBuilder,
property_owner: &Option<LuaSemanticDeclId>,
typ: &LuaType,
) -> (Option<CompletionColorInfo>, Option<String>) {
let mut color = color_info_from_type(typ);
let should_inspect_color =
color.is_none() && (is_color_type(typ) || matches!(typ, LuaType::Unknown));
let should_inspect_constructor =
is_gmod_literal_constructor_type(typ) || matches!(typ, LuaType::Unknown);
if !should_inspect_color && !should_inspect_constructor {
return (color, None);
}
let Some(LuaSemanticDeclId::Member(member_id)) = property_owner.as_ref() else {
return (color, None);
};
let value_expr = get_member_value_expr(builder.semantic_model.get_db(), *member_id);
if should_inspect_color {
color = value_expr.as_ref().and_then(color_info_from_expr);
}
let constructor_literal_detail = if color.is_none() && should_inspect_constructor {
value_expr
.as_ref()
.and_then(gmod_constructor_literal_detail)
} else {
None
};
(color, constructor_literal_detail)
}
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 add_signature_overloads(
builder: &mut CompletionBuilder,
property_owner: &Option<LuaSemanticDeclId>,
typ: &LuaType,
call_display: CallDisplay,
deprecated: Option<bool>,
kind: Option<lsp_types::CompletionItemKind>,
label: String,
overload_count: Option<usize>,
description_hint: Option<&str>,
) -> Option<()> {
let signature_id = match typ {
LuaType::Signature(signature_id) => signature_id,
_ => return None,
};
let overloads = builder
.semantic_model
.get_db()
.get_signature_index()
.get(signature_id)?
.overloads
.clone();
overloads
.into_iter()
.enumerate()
.for_each(|(index, overload)| {
let typ = LuaType::DocFunction(overload);
let description =
apply_description_hint(get_description(builder, &typ), description_hint);
let detail = get_detail(builder, &typ, call_display);
let data = if let Some(id) = &property_owner {
CompletionData::from_overload(builder, id.clone(), index, overload_count)
} else {
None
};
let completion_item = CompletionItem {
label: label.clone(),
kind,
data,
label_details: Some(lsp_types::CompletionItemLabelDetails {
detail,
description,
}),
deprecated,
tags: get_completion_tags(builder, deprecated),
..Default::default()
};
builder.add_completion_item(completion_item);
});
Some(())
}
fn get_member_completion_kind(
db: &DbIndex,
property_owner: &Option<LuaSemanticDeclId>,
typ: &LuaType,
status: CompletionTriggerStatus,
) -> lsp_types::CompletionItemKind {
let type_kind = get_completion_kind(typ);
if type_kind != lsp_types::CompletionItemKind::FUNCTION {
if is_global_table_namespace_member(db, property_owner, typ) {
return lsp_types::CompletionItemKind::INTERFACE;
}
return match type_kind {
lsp_types::CompletionItemKind::CLASS
| lsp_types::CompletionItemKind::MODULE
| lsp_types::CompletionItemKind::STRUCT
| lsp_types::CompletionItemKind::TYPE_PARAMETER => type_kind,
_ => lsp_types::CompletionItemKind::FIELD,
};
}
if status == CompletionTriggerStatus::Colon || is_colon_defined_function(db, typ) {
lsp_types::CompletionItemKind::METHOD
} else {
type_kind
}
}
fn is_completion_callable_type(typ: &LuaType) -> bool {
typ.is_function() || get_completion_kind(typ) == lsp_types::CompletionItemKind::FUNCTION
}
fn is_global_table_namespace_member(
db: &DbIndex,
property_owner: &Option<LuaSemanticDeclId>,
typ: &LuaType,
) -> bool {
if !is_table_namespace_type(typ) {
return false;
}
let Some(LuaSemanticDeclId::Member(member_id)) = property_owner else {
return false;
};
let member_index = db.get_member_index();
if let Some(LuaMemberOwner::GlobalPath(_)) = member_index.get_current_owner(member_id) {
return true;
}
member_index
.get_member(member_id)
.and_then(|member| member.get_global_id())
.is_some()
}
fn is_colon_defined_function(db: &DbIndex, typ: &LuaType) -> bool {
match typ {
LuaType::Signature(signature_id) => db
.get_signature_index()
.get(signature_id)
.is_some_and(|signature| signature.is_colon_define),
LuaType::DocFunction(func) => func.is_colon_define(),
_ => false,
}
}
fn get_call_show(
db: &DbIndex,
typ: &LuaType,
status: CompletionTriggerStatus,
) -> Option<CallDisplay> {
let (colon_call, colon_define) = match typ {
LuaType::Signature(sig_id) => {
let signature = db.get_signature_index().get(sig_id)?;
let colon_define = signature.is_colon_define;
let colon_call = status == CompletionTriggerStatus::Colon;
(colon_call, colon_define)
}
LuaType::DocFunction(func) => {
let colon_define = func.is_colon_define();
let colon_call = status == CompletionTriggerStatus::Colon;
(colon_call, colon_define)
}
_ => return None,
};
match (colon_call, colon_define) {
(false, true) => Some(CallDisplay::AddSelf),
(true, false) => Some(CallDisplay::RemoveFirst),
_ => Some(CallDisplay::None),
}
}
fn resolve_function_params(
builder: &mut CompletionBuilder,
completion_item: &mut CompletionItem,
typ: &LuaType,
call_display: CallDisplay,
) -> Option<()> {
if completion_item.insert_text.is_some() || completion_item.text_edit.is_some() {
return None;
}
let new_text =
get_resolve_function_params_str(builder.semantic_model.get_db(), typ, call_display)?;
let index_expr = builder
.trigger_token
.parent_ancestors()
.find_map(LuaIndexExpr::cast)?;
let func_stat = index_expr.get_parent::<LuaFuncStat>()?;
if func_stat.get_closure().is_some() {
return None;
}
let next_sibling = func_stat.syntax().next_sibling()?;
let assign_stat = LuaAssignStat::cast(next_sibling)?;
let paren_expr = assign_stat.child::<LuaParenExpr>()?;
if paren_expr.get_expr().is_some() {
return None;
}
let left_paren = paren_expr.token::<LuaGeneralToken>()?;
if left_paren.get_token_kind() != LuaTokenKind::TkLeftParen {
return None;
}
let document = builder.semantic_model.get_document();
let add_range = left_paren.syntax().text_range();
let mut lsp_add_range = document.to_lsp_range(add_range)?;
lsp_add_range.start.character += 1;
if new_text.is_empty() {
return None;
}
completion_item.additional_text_edits = Some(vec![lsp_types::TextEdit {
range: lsp_add_range,
new_text,
}]);
Some(())
}
fn get_resolve_function_params_str(
db: &DbIndex,
typ: &LuaType,
display: CallDisplay,
) -> Option<String> {
let mut params_str = match typ {
LuaType::DocFunction(f) => f
.get_params()
.iter()
.map(|param| param.0.clone())
.collect::<Vec<_>>(),
LuaType::Signature(signature_id) => {
db.get_signature_index().get(signature_id)?.params.clone()
}
_ => return None,
};
match display {
CallDisplay::AddSelf => {
params_str.insert(0, "self".to_string());
}
CallDisplay::RemoveFirst => {
if !params_str.is_empty() {
params_str.remove(0);
}
}
_ => {}
}
Some(params_str.join(", "))
}
fn try_add_alias_completion_item_new(
builder: &mut CompletionBuilder,
member_info: &LuaMemberInfo,
completion_item: &CompletionItem,
label: &String,
) -> Option<bool> {
let alias_label = get_index_alias_name(&builder.semantic_model, member_info)?;
let mut alias_completion_item = completion_item.clone();
alias_completion_item.label = alias_label;
alias_completion_item.insert_text = Some(label.clone());
alias_completion_item.filter_text = Some(format!(
"{} {}",
alias_completion_item.label.as_str(),
label.as_str()
));
let index_hint = t!("completion.index %{label}", label = label).to_string();
let label_details = alias_completion_item
.label_details
.get_or_insert_with(Default::default);
label_details.description = match label_details.description.take() {
Some(desc) => Some(format!("({}) {} ", index_hint, desc)),
None => Some(index_hint),
};
builder.add_completion_item(alias_completion_item)?;
Some(true)
}
pub fn get_index_alias_name(
semantic_model: &SemanticModel,
member_info: &LuaMemberInfo,
) -> Option<String> {
let db = semantic_model.get_db();
let LuaMemberKey::Integer(_) = member_info.key else {
return None;
};
let property_owner_id = member_info.property_owner_id.as_ref()?;
let LuaSemanticDeclId::Member(member_id) = property_owner_id else {
return None;
};
let common_property = match db.get_property_index().get_property(property_owner_id) {
Some(common_property) => common_property,
None => {
let member = db.get_member_index().get_member(member_id)?;
let signature_id =
try_extract_signature_id_from_field(semantic_model.get_db(), member)?;
db.get_property_index()
.get_property(&LuaSemanticDeclId::Signature(signature_id))?
}
};
let alias_label = common_property
.find_attribute_use("index_alias")?
.args
.first()
.and_then(|(_, typ)| typ.as_ref())
.and_then(|param| match param {
LuaType::DocStringConst(s) => Some(s.as_ref()),
_ => None,
})?
.to_string();
Some(alias_label)
}