emmylua_ls 0.25.1

A language server for emmylua.
Documentation
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> {
    /// Type description, does not include overload
    pub primary: MarkedString,
    /// Full path of the class
    pub location_path: Option<MarkedString>,
    /// Function overload signatures
    pub signature_overload: Option<Vec<HoverSignatureOverload>>,
    /// Annotation descriptions, including function parameters and return values
    pub annotation_description: Vec<MarkedString>,
    /// 一些类型的完整追加显示, 通常是 @alias
    pub type_expansion: Option<Vec<String>>,
    /// For `@see` and unknown tags tags
    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!(
                    "{}{} `{}`",
                    "&nbsp;&nbsp;",
                    "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));
                    }
                }
            }
        }
    }
}