use emmylua_code_analysis::{DbIndex, LuaMemberInfo, LuaMemberKey, LuaSemanticDeclId, LuaType};
use emmylua_parser::{
LuaAssignStat, LuaAstNode, LuaAstToken, LuaFuncStat, LuaGeneralToken, LuaIndexExpr,
LuaParenExpr, LuaTokenKind,
};
use lsp_types::CompletionItem;
use crate::handlers::completion::{
completion_builder::CompletionBuilder, completion_data::CompletionData,
providers::get_function_remove_nil,
};
use super::{
check_visibility, get_completion_kind, get_description, get_detail, is_deprecated, CallDisplay,
};
#[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,
function_overload_count: Option<usize>,
) -> 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 label = match status {
CompletionTriggerStatus::Dot => match member_key {
LuaMemberKey::Name(name) => name.to_string(),
LuaMemberKey::Integer(index) => format!("[{}]", index),
_ => return None,
},
CompletionTriggerStatus::Colon => match member_key {
LuaMemberKey::Name(name) => name.to_string(),
_ => return None,
},
CompletionTriggerStatus::InString => match member_key {
LuaMemberKey::Name(name) => name.to_string(),
_ => return None,
},
CompletionTriggerStatus::LeftBracket => match member_key {
LuaMemberKey::Name(name) => format!("\"{}\"", name.to_string()),
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);
if status == CompletionTriggerStatus::Colon && !remove_nil_type.is_function() {
return None;
}
let completion_data = if let Some(id) = &property_owner {
if let Some(index) = member_info.overload_index {
CompletionData::from_overload(
builder,
id.clone().into(),
index,
function_overload_count,
)
} else {
CompletionData::from_property_owner_id(
builder,
id.clone().into(),
function_overload_count,
)
}
} else {
None
};
let call_display = get_call_show(builder.semantic_model.get_db(), &remove_nil_type, status)
.unwrap_or(CallDisplay::None);
let detail = get_detail(builder, &remove_nil_type, call_display);
let description = get_description(builder, &remove_nil_type);
let deprecated = if let Some(id) = &property_owner {
Some(is_deprecated(builder, id.clone()))
} else {
None
};
let mut completion_item = CompletionItem {
label: label.clone(),
kind: Some(get_completion_kind(&remove_nil_type)),
data: completion_data,
label_details: Some(lsp_types::CompletionItemLabelDetails {
detail,
description,
}),
deprecated,
..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,
);
}
builder.add_completion_item(completion_item)?;
add_signature_overloads(
builder,
property_owner,
&remove_nil_type,
call_display,
deprecated,
label,
function_overload_count,
);
Some(())
}
fn add_signature_overloads(
builder: &mut CompletionBuilder,
property_owner: &Option<LuaSemanticDeclId>,
typ: &LuaType,
call_display: CallDisplay,
deprecated: Option<bool>,
label: String,
overload_count: Option<usize>,
) -> 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 = get_description(builder, &typ);
let detail = get_detail(builder, &typ, call_display);
let data = if let Some(id) = &property_owner {
CompletionData::from_overload(builder, id.clone().into(), index, overload_count)
} else {
None
};
let completion_item = CompletionItem {
label: label.clone(),
kind: Some(get_completion_kind(&typ)),
data,
label_details: Some(lsp_types::CompletionItemLabelDetails {
detail,
description,
}),
deprecated,
..Default::default()
};
builder.add_completion_item(completion_item);
});
Some(())
}
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(&typ, call_display)?;
let index_expr = LuaIndexExpr::cast(builder.trigger_token.parent()?)?;
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 let Some(_) = paren_expr.get_expr() {
return None;
}
let left_paren = paren_expr.token::<LuaGeneralToken>()?;
if left_paren.get_token_kind() != LuaTokenKind::TkLeftParen.into() {
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: new_text,
}]);
Some(())
}
fn get_resolve_function_params_str(typ: &LuaType, display: CallDisplay) -> Option<String> {
match typ {
LuaType::DocFunction(f) => {
let mut params_str = f
.get_params()
.iter()
.map(|param| param.0.clone())
.collect::<Vec<_>>();
match display {
CallDisplay::AddSelf => {
params_str.insert(0, "self".to_string());
}
CallDisplay::RemoveFirst => {
if !params_str.is_empty() {
params_str.remove(0);
}
}
_ => {}
}
Some(format!("{}", params_str.join(", ")))
}
_ => None,
}
}