use std::collections::HashMap;
use emmylua_code_analysis::{
LuaSignatureId, LuaType, LuaUnionType, RenderLevel, SemanticModel, format_union_type,
humanize_type,
};
use emmylua_parser::{LuaAstNode, LuaClosureExpr};
use itertools::Itertools;
use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, InlayHintLabelPart, Location};
pub fn build_closure_hint(
semantic_model: &SemanticModel,
result: &mut Vec<InlayHint>,
closure: LuaClosureExpr,
) -> Option<()> {
if !semantic_model.get_emmyrc().hint.param_hint {
return Some(());
}
let file_id = semantic_model.get_file_id();
let signature_id = LuaSignatureId::from_closure(file_id, &closure);
let signature = semantic_model
.get_db()
.get_signature_index()
.get(&signature_id)?;
let lua_params = closure.get_params_list()?;
let signature_params = signature.get_type_params();
let mut lua_params_map = 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();
lua_params_map.insert(name, param);
} else if param.is_dots() {
lua_params_map.insert("...".to_string(), param);
}
}
let document = semantic_model.get_document();
for (signature_param_name, typ) in &signature_params {
if let Some(typ) = typ {
if typ.is_any() {
continue;
}
if let Some(lua_param) = lua_params_map.get(signature_param_name) {
let lsp_range = document.to_lsp_range(lua_param.get_range())?;
let mut label_parts = build_label_parts(semantic_model, &typ);
if label_parts.is_empty() {
let typ_desc = format!(
": {}",
hint_humanize_type(semantic_model, &typ, RenderLevel::Simple)
);
label_parts.push(InlayHintLabelPart {
value: typ_desc,
location: Some(
get_type_location(semantic_model, typ, 0)
.unwrap_or(Location::new(document.get_uri(), lsp_range)),
),
..Default::default()
});
}
let hint = InlayHint {
kind: Some(InlayHintKind::TYPE),
label: InlayHintLabel::LabelParts(label_parts),
position: lsp_range.end,
text_edits: None,
tooltip: None,
padding_left: Some(true),
padding_right: None,
data: None,
};
result.push(hint);
}
}
}
Some(())
}
pub fn build_label_parts(semantic_model: &SemanticModel, typ: &LuaType) -> Vec<InlayHintLabelPart> {
let mut parts: Vec<InlayHintLabelPart> = Vec::new();
match typ {
LuaType::Union(union) => {
for typ in union.into_vec() {
if let Some(part) = get_part(semantic_model, &typ) {
parts.push(part);
}
}
}
_ => {
if let Some(part) = get_part(semantic_model, typ) {
parts.push(part);
}
}
}
let parts: Vec<InlayHintLabelPart> = parts
.into_iter()
.unique_by(|part| part.value.clone())
.collect();
let mut normal_parts = Vec::new();
let mut nil_parts = Vec::new();
for part in parts {
if part.value == "?" {
nil_parts.push(part);
} else {
normal_parts.push(part);
}
}
normal_parts.append(&mut nil_parts);
let mut result = Vec::new();
for (i, part) in normal_parts.into_iter().enumerate() {
let mut part = part;
if part.value != "?" {
part.value = format!("{}{}", if i == 0 { ": " } else { "|" }, part.value);
}
result.push(part);
}
if result.len() == 1 && result[0].value == "?" {
result[0].value = ": nil".to_string();
}
result
}
fn get_part(semantic_model: &SemanticModel, typ: &LuaType) -> Option<InlayHintLabelPart> {
match typ {
LuaType::Union(_) => None,
LuaType::Nil => {
return Some(InlayHintLabelPart {
value: "?".to_string(),
location: get_type_location(semantic_model, typ, 0),
..Default::default()
});
}
_ => {
let value = hint_humanize_type(semantic_model, typ, RenderLevel::Simple);
let location = get_type_location(semantic_model, typ, 0);
return Some(InlayHintLabelPart {
value,
location,
..Default::default()
});
}
}
}
fn get_type_location(
semantic_model: &SemanticModel,
typ: &LuaType,
depth: usize,
) -> Option<Location> {
if depth > 10 {
return None;
}
match typ {
LuaType::Ref(id) | LuaType::Def(id) => {
let type_decl = semantic_model
.get_db()
.get_type_index()
.get_type_decl(&id)?;
let location = type_decl.get_locations().first()?;
let document = semantic_model.get_document_by_file_id(location.file_id)?;
let lsp_range = document.to_lsp_range(location.range)?;
Some(Location::new(document.get_uri(), lsp_range))
}
LuaType::Generic(generic) => {
let base_type_id = generic.get_base_type_id();
get_type_location(semantic_model, &LuaType::Ref(base_type_id), depth + 1)
}
LuaType::Array(array_type) => {
get_type_location(semantic_model, array_type.get_base(), depth + 1)
}
LuaType::Any => get_base_type_location(semantic_model, "any"),
LuaType::Nil => get_base_type_location(semantic_model, "nil"),
LuaType::Unknown => get_base_type_location(semantic_model, "unknown"),
LuaType::Userdata => get_base_type_location(semantic_model, "userdata"),
LuaType::Function => get_base_type_location(semantic_model, "function"),
LuaType::Thread => get_base_type_location(semantic_model, "thread"),
LuaType::Table => get_base_type_location(semantic_model, "table"),
_ if typ.is_string() => get_base_type_location(semantic_model, "string"),
_ if typ.is_integer() => get_base_type_location(semantic_model, "integer"),
_ if typ.is_number() => get_base_type_location(semantic_model, "number"),
_ if typ.is_boolean() => get_base_type_location(semantic_model, "boolean"),
_ => None,
}
}
fn get_base_type_location(semantic_model: &SemanticModel, name: &str) -> Option<Location> {
let type_decl = semantic_model
.get_db()
.get_type_index()
.find_type_decl(semantic_model.get_file_id(), name)?;
let location = type_decl.get_locations().first()?;
let document = semantic_model.get_document_by_file_id(location.file_id)?;
let lsp_range = document.to_lsp_range(location.range)?;
Some(Location::new(document.get_uri(), lsp_range))
}
fn hint_humanize_type(semantic_model: &SemanticModel, typ: &LuaType, level: RenderLevel) -> String {
match typ {
LuaType::Ref(id) | LuaType::Def(id) => {
let namespace = semantic_model
.get_db()
.get_type_index()
.get_file_namespace(&semantic_model.get_file_id());
if let Some(namespace) = namespace {
let id_name = id.get_name();
let namespace_prefix = format!("{}.", namespace);
if id_name.starts_with(&namespace_prefix) {
id_name[namespace_prefix.len()..].to_string()
} else {
id_name.to_string()
}
} else {
id.get_name().to_string()
}
}
LuaType::Generic(generic) => {
let base_type_id = generic.get_base_type_id();
let base_type_name =
hint_humanize_type(semantic_model, &LuaType::Ref(base_type_id), level);
let generic_params = generic
.get_params()
.iter()
.map(|ty| hint_humanize_type(semantic_model, ty, level.next_level()))
.collect::<Vec<_>>()
.join(",");
format!("{}<{}>", base_type_name, generic_params)
}
LuaType::Union(union) => hint_humanize_union_type(semantic_model, union, level),
_ => humanize_type(semantic_model.get_db(), typ, level),
}
}
fn hint_humanize_union_type(
semantic_model: &SemanticModel,
union: &LuaUnionType,
level: RenderLevel,
) -> String {
format_union_type(union, level, |ty, _| {
hint_humanize_type(semantic_model, ty, level)
})
}