use emmylua_code_analysis::{
LuaMember, LuaMemberOwner, LuaSemanticDeclId, LuaType, RenderLevel, SemanticModel,
};
use emmylua_parser::LuaSyntaxToken;
use lsp_types::{Hover, HoverContents, MarkedString, MarkupContent};
use crate::handlers::hover::humanize_types::{
DescriptionInfo, extract_description_from_property_owner,
};
use super::build_hover::{add_signature_param_description, add_signature_ret_description};
#[derive(Debug)]
pub struct HoverBuilder<'a> {
pub primary: MarkedString,
pub location_path: Option<MarkedString>,
pub signature_overload: Option<Vec<HoverSignatureOverload>>,
pub annotation_description: Vec<MarkedString>,
pub type_expansion: Option<Vec<String>>,
tag_content: Option<Vec<(String, String)>>,
trigger_token: Option<LuaSyntaxToken>,
pub semantic_model: &'a SemanticModel<'a>,
pub detail_render_level: RenderLevel,
pub is_completion: bool,
}
impl<'a> HoverBuilder<'a> {
pub fn new(
semantic_model: &'a SemanticModel,
token: Option<LuaSyntaxToken>,
is_completion: bool,
) -> Self {
let detail_render_level =
if let Some(custom_detail) = semantic_model.get_emmyrc().hover.custom_detail {
RenderLevel::CustomDetailed(custom_detail)
} else {
RenderLevel::Detailed
};
Self {
semantic_model,
primary: MarkedString::String("".to_string()),
location_path: None,
signature_overload: None,
annotation_description: Vec::new(),
is_completion,
trigger_token: token,
type_expansion: None,
tag_content: None,
detail_render_level,
}
}
pub fn set_type_description(&mut self, type_description: String) {
self.primary = MarkedString::from_language_code("lua".to_string(), type_description);
}
pub fn set_location_path(&mut self, owner_member: Option<&LuaMember>) {
if let Some(owner_member) = owner_member {
let owner_id = self
.semantic_model
.get_db()
.get_member_index()
.get_current_owner(&owner_member.get_id());
if let Some(LuaMemberOwner::Type(ty)) = owner_id
&& ty.get_name() != ty.get_simple_name()
{
self.location_path = Some(MarkedString::from_markdown(format!(
"{}{} `{}`",
" ",
"in class",
ty.get_name()
)));
}
}
}
pub fn add_signature_overload(&mut self, signature_overload: String, comment: Option<String>) {
if signature_overload.is_empty() {
return;
}
if self.signature_overload.is_none() {
self.signature_overload = Some(Vec::new());
}
self.signature_overload
.as_mut()
.unwrap()
.push(HoverSignatureOverload::new(signature_overload, comment));
}
pub fn add_type_expansion(&mut self, type_expansion: String) {
if type_expansion.is_empty() {
return;
}
if self.type_expansion.is_none() {
self.type_expansion = Some(Vec::new());
}
self.type_expansion.as_mut().unwrap().push(type_expansion);
}
pub fn get_type_expansion_count(&self) -> usize {
if let Some(type_expansion) = &self.type_expansion {
type_expansion.len()
} else {
0
}
}
pub fn pop_type_expansion(&mut self, start: usize, end: usize) -> Option<Vec<String>> {
if let Some(type_expansion) = &mut self.type_expansion {
let mut result = Vec::new();
result.extend(type_expansion.drain(start..end));
Some(result)
} else {
None
}
}
pub fn add_annotation_description(&mut self, annotation_description: String) {
if annotation_description.is_empty() {
return;
}
self.annotation_description
.push(MarkedString::from_markdown(annotation_description));
}
pub fn add_description(&mut self, property_owner: &LuaSemanticDeclId) -> Option<()> {
self.add_description_from_info(extract_description_from_property_owner(
self.semantic_model,
property_owner,
))
}
pub fn add_description_from_info(&mut self, type_desc: Option<DescriptionInfo>) -> Option<()> {
if let Some(desc_info) = type_desc {
if let Some(description) = desc_info.description {
self.add_annotation_description(description);
}
if let Some(tag_content) = desc_info.tag_content {
self.tag_content = Some(tag_content);
}
Some(())
} else {
None
}
}
pub fn add_signature_params_rets_description(&mut self, typ: LuaType) {
if let LuaType::Signature(signature_id) = typ {
add_signature_param_description(
self.semantic_model.get_db(),
&mut self.annotation_description,
signature_id,
);
add_signature_ret_description(
self.semantic_model.get_db(),
&mut self.annotation_description,
signature_id,
);
}
}
pub fn build_hover_result(&self, range: Option<lsp_types::Range>) -> Option<Hover> {
let header = {
let mut header = String::new();
match &self.primary {
MarkedString::String(s) => {
header.push_str(&format!("\n{}\n", s));
}
MarkedString::LanguageString(s) => {
header.push_str(&format!("\n```{}\n{}\n```\n", s.language, s.value));
}
}
if let Some(location_path) = &self.location_path
&& let MarkedString::String(s) = location_path
{
header.push_str(&format!("\n{}\n", s));
}
header
};
let description_content = {
let mut content = String::new();
for marked_string in &self.annotation_description {
match marked_string {
MarkedString::String(s) => {
content.push_str(&format!("\n{}\n", s));
}
MarkedString::LanguageString(s) => {
content.push_str(&format!("\n```{}\n{}\n```\n", s.language, s.value));
}
}
}
if let Some(tag_content) = &self.tag_content {
if !tag_content.is_empty() {
content.push_str("\n---\n");
}
for (tag_name, description) in tag_content {
content.push_str(&format!("\n@*{}* {}\n", tag_name, description));
}
}
content
};
let expansion = {
let mut expansion = String::new();
if let Some(signature_overload) = &self.signature_overload {
expansion.push_str("\n---\n");
for overload in signature_overload {
overload.append_markdown(&mut expansion);
}
}
if let Some(type_expansion) = &self.type_expansion {
for type_expansion in type_expansion {
expansion.push_str(&format!("\n```{}\n{}\n```\n", "lua", type_expansion));
}
}
expansion
};
let mut result = String::new();
result.push_str(&header);
if !description_content.is_empty() || !expansion.is_empty() {
result.push_str("\n---\n");
}
result.push_str(&description_content);
result.push_str(&expansion);
result = result.trim().to_string();
Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: lsp_types::MarkupKind::Markdown,
value: result,
}),
range,
})
}
pub fn get_trigger_token(&self) -> Option<LuaSyntaxToken> {
self.trigger_token.clone()
}
}
#[derive(Debug, Clone)]
pub struct HoverSignatureOverload {
pub signature: MarkedString,
pub comment: Option<String>,
}
impl HoverSignatureOverload {
fn new(signature: String, comment: Option<String>) -> Self {
Self {
signature: MarkedString::from_language_code("lua".to_string(), signature),
comment: comment.filter(|comment| !comment.trim().is_empty()),
}
}
fn append_markdown(&self, content: &mut String) {
const LIMIT: usize = 80;
let inline_comment = self
.comment
.as_deref()
.filter(|comment| !comment.chars().any(|ch| ch == '\n' || ch == '\r'));
match &self.signature {
MarkedString::String(s) => {
if let Some(comment) = inline_comment {
if s.chars().count() <= LIMIT {
content.push_str(&format!("\n{} -- {}\n", s, comment));
} else {
content.push_str(&format!("\n{}\n-- {}\n", s, comment));
}
} else {
content.push_str(&format!("\n{}\n", s));
if let Some(comment) = self.comment.as_deref() {
content.push_str(&format!("\n{}\n", comment));
}
}
}
MarkedString::LanguageString(s) => {
if let Some(comment) = inline_comment {
if s.value.chars().count() <= LIMIT {
content.push_str(&format!(
"\n```{}\n{} -- {}\n```\n",
s.language, s.value, comment
));
} else {
content.push_str(&format!(
"\n```{}\n{}\n-- {}\n```\n",
s.language, s.value, comment
));
}
} else {
content.push_str(&format!("\n```{}\n{}\n```\n", s.language, s.value));
if let Some(comment) = self.comment.as_deref() {
content.push_str(&format!("\n{}\n", comment));
}
}
}
}
}
}