use std::collections::HashSet;
use glua_code_analysis::{
InferGuard, LuaMemberInfo, LuaMemberKey, LuaType, get_real_type,
infer_table_field_value_should_be,
};
use glua_parser::{LuaAst, LuaAstNode, LuaKind, LuaTableExpr, LuaTableField, LuaTokenKind};
use lsp_types::{CompletionItem, InsertTextFormat, InsertTextMode};
use rowan::NodeOrToken;
use crate::handlers::completion::{
add_completions::{check_visibility, get_completion_tags, is_deprecated},
completion_builder::CompletionBuilder,
completion_data::CompletionData,
providers::function_provider::dispatch_type,
};
pub fn add_completion(builder: &mut CompletionBuilder) -> Option<()> {
add_table_field_key_completion(builder);
add_table_field_value_completion(builder);
Some(())
}
fn add_table_field_key_completion(builder: &mut CompletionBuilder) -> Option<()> {
if !can_add_key_completion(builder) {
return None;
}
let prev_token = builder.trigger_token.prev_token()?;
if builder.trigger_token.kind() == LuaKind::Token(LuaTokenKind::TkWhitespace)
&& prev_token.kind() == LuaKind::Token(LuaTokenKind::TkAssign)
{
return None;
}
let node = LuaAst::cast(builder.trigger_token.parent()?)?;
let table_expr = match node {
LuaAst::LuaTableExpr(table_expr) => Some(table_expr),
LuaAst::LuaNameExpr(name_expr) => name_expr
.get_parent::<LuaTableField>()?
.get_parent::<LuaTableExpr>(),
_ => None,
}?;
let table_type = builder
.semantic_model
.infer_table_should_be(table_expr.clone())?;
let member_infos = builder
.semantic_model
.get_member_infos_at_offset(&table_type, builder.position_offset)?;
let mut duplicated_set = HashSet::new();
for field in table_expr.get_fields() {
let key = field.get_field_key();
if let Some(key) = key {
duplicated_set.insert(key.get_path_part());
}
}
for member_info in member_infos {
if duplicated_set.contains(&member_info.key.to_path()) {
continue;
}
duplicated_set.insert(member_info.key.to_path());
add_field_key_completion(builder, member_info);
}
builder.stop_here();
Some(())
}
fn can_add_key_completion(builder: &mut CompletionBuilder) -> bool {
if builder.is_cancelled() {
return false;
}
if builder.is_space_trigger_character {
return false;
}
if let Some(NodeOrToken::Node(node)) = builder.trigger_token.prev_sibling_or_token()
&& let Some(LuaAst::LuaComment(_)) = LuaAst::cast(node)
{
return false;
}
true
}
fn add_field_key_completion(
builder: &mut CompletionBuilder,
member_info: LuaMemberInfo,
) -> Option<()> {
let property_owner = &member_info.property_owner_id;
if let Some(property_owner) = &property_owner {
check_visibility(builder, property_owner.clone())?;
}
let name = match member_info.key {
LuaMemberKey::Name(name) => name.to_string(),
LuaMemberKey::Integer(index) => format!("[{}]", index),
_ => return None,
};
let typ = member_info.typ;
let (label, insert_text, insert_text_format) = {
let is_nullable = if typ.is_nullable() { "?" } else { "" };
if in_env(builder, &name, &typ).is_some() {
(
format!(
"{name}{nullable} = {name},",
name = name,
nullable = is_nullable,
),
format!("{name} = ${{1:{name}}},", name = name),
Some(InsertTextFormat::SNIPPET),
)
} else {
let space = if typ.is_function() { "" } else { " " };
(
format!(
"{name}{nullable} ={space}",
name = name,
nullable = is_nullable,
space = space
),
format!("{name} ={space}", name = name, space = space),
None,
)
}
};
let property_owner = &member_info.property_owner_id;
if let Some(property_owner) = &property_owner {
check_visibility(builder, property_owner.clone())?;
}
let data = if let Some(id) = &property_owner {
CompletionData::from_property_owner_id(builder, id.clone(), None)
} else {
None
};
let deprecated = property_owner
.as_ref()
.map(|id| is_deprecated(builder, id.clone()));
let completion_item = CompletionItem {
label,
kind: Some(lsp_types::CompletionItemKind::PROPERTY),
data,
deprecated,
tags: get_completion_tags(builder, deprecated),
insert_text: Some(insert_text),
insert_text_format,
..Default::default()
};
builder.add_completion_item(completion_item);
Some(())
}
fn in_env(builder: &mut CompletionBuilder, target_name: &str, target_type: &LuaType) -> Option<()> {
let file_id = builder.semantic_model.get_file_id();
let decl_tree = builder
.semantic_model
.get_db()
.get_decl_index()
.get_decl_tree(&file_id)?;
let local_env = decl_tree.get_env_decls(builder.trigger_token.text_range().start())?;
let global_env = builder
.semantic_model
.get_db()
.get_global_index()
.get_all_global_decl_ids()
.into_iter()
.filter(|id| {
!builder
.semantic_model
.get_db()
.get_module_index()
.is_std(&id.file_id)
})
.collect();
let all_env = [local_env, global_env].concat();
for decl_id in all_env.iter() {
if builder.is_cancelled() {
return None;
}
let decl = builder
.semantic_model
.get_db()
.get_decl_index()
.get_decl(decl_id)?;
let (name, typ) = {
(
decl.get_name().to_string(),
builder
.semantic_model
.get_db()
.get_type_index()
.get_type_cache(&(*decl_id).into())
.map(|cache| cache.as_type().clone())
.unwrap_or(LuaType::Unknown),
)
};
if name == target_name && builder.semantic_model.type_check(target_type, &typ).is_ok() {
return Some(());
}
}
None
}
fn add_table_field_value_completion(builder: &mut CompletionBuilder) -> Option<()> {
if builder.is_cancelled() {
return None;
}
let mut parent = if builder.trigger_token.kind() == LuaTokenKind::TkWhitespace.into() {
builder.trigger_token.prev_token()?.parent()?
} else {
builder.trigger_token.parent()?
};
for _ in 0..3 {
match LuaAst::cast(parent.clone())? {
LuaAst::LuaTableField(field) => {
if field.is_assign_field() {
let table_expr = field.get_parent::<LuaTableExpr>()?;
let table_type = builder
.semantic_model
.infer_table_should_be(table_expr.clone())?;
let key = builder
.semantic_model
.get_member_key(&field.get_field_key()?)?;
let member_infos = builder
.semantic_model
.get_member_infos_at_offset(&table_type, builder.position_offset)?;
let member_info = member_infos.iter().find(|m| m.key == key)?;
if add_field_value_completion(builder, member_info.clone()).is_some() {
builder.stop_here();
}
} else {
let table_field_should = infer_table_field_value_should_be(
builder.semantic_model.get_db(),
&mut builder.semantic_model.get_cache().borrow_mut(),
field,
)
.ok()?;
dispatch_type(builder, table_field_should, &InferGuard::new())?;
}
return Some(());
}
_ => parent = parent.parent()?,
}
}
Some(())
}
fn add_field_value_completion(
builder: &mut CompletionBuilder,
member_info: LuaMemberInfo,
) -> Option<()> {
let real_type = get_real_type(builder.semantic_model.get_db(), &member_info.typ)?;
if real_type.is_function() {
let label_detail = get_function_detail(builder, real_type);
let item = CompletionItem {
label: "fun".to_string(),
label_details: Some(lsp_types::CompletionItemLabelDetails {
detail: label_detail.clone(),
description: None,
}),
kind: Some(lsp_types::CompletionItemKind::SNIPPET),
insert_text: Some(format!(
"function{}\n\t${{0}}\nend",
label_detail.unwrap_or_default()
)),
insert_text_format: Some(InsertTextFormat::SNIPPET),
insert_text_mode: Some(InsertTextMode::ADJUST_INDENTATION),
..CompletionItem::default()
};
return builder.add_completion_item(item);
} else {
dispatch_type(builder, real_type.clone(), &InferGuard::new())?;
}
None
}
fn get_function_detail(builder: &CompletionBuilder, typ: &LuaType) -> Option<String> {
match typ {
LuaType::Signature(signature_id) => {
let signature = builder
.semantic_model
.get_db()
.get_signature_index()
.get(signature_id)?;
let params_str = signature
.get_type_params()
.iter()
.map(|param| param.0.clone())
.collect::<Vec<_>>();
Some(format!("({})", params_str.join(", ")))
}
LuaType::DocFunction(f) => {
let params_str = f
.get_params()
.iter()
.map(|param| param.0.clone())
.collect::<Vec<_>>();
Some(format!("({})", params_str.join(", ")))
}
_ => None,
}
}