glua_ls 1.0.27

Language server for Garry's Mod Lua (GLua).
Documentation
use glua_code_analysis::{
    EmmyrcFilenameConvention, LuaSemanticDeclId, LuaType, ModuleInfo, check_export_visibility,
};
use glua_parser::{LuaAstNode, LuaNameExpr};
use lsp_types::{CompletionItem, Position};

use crate::{
    handlers::{
        command::make_auto_require,
        completion::{
            add_completions::get_completion_kind, completion_builder::CompletionBuilder,
            completion_data::CompletionData,
        },
    },
    util::{file_name_convert, module_name_convert},
};

pub fn add_completion(builder: &mut CompletionBuilder) -> Option<()> {
    if builder.is_cancelled() {
        return None;
    }

    let enable = builder.semantic_model.get_emmyrc().completion.auto_require;
    if !enable {
        return None;
    }

    let name_expr = LuaNameExpr::cast(builder.trigger_token.parent()?)?;
    // optimize for large project
    let prefix = name_expr.get_name_text()?.to_lowercase();
    let emmyrc = builder.semantic_model.get_emmyrc();
    let file_conversion = emmyrc.completion.auto_require_naming_convention;
    let version_number = emmyrc.runtime.version.to_lua_version_number();
    let file_id = builder.semantic_model.get_file_id();
    let module_index = builder.semantic_model.get_db().get_module_index();
    let module_infos = module_index.get_module_infos();
    let range = builder.trigger_token.text_range();
    let document = builder.semantic_model.get_document();
    let lsp_position = document.to_lsp_range(range)?.start;

    let mut completions = Vec::new();
    for module_info in module_infos {
        if module_info.is_visible(&version_number)
            && module_info.file_id != file_id
            && module_info.export_type.is_some()
            && !module_index.is_std(&module_info.file_id)
        {
            add_module_completion_item(
                builder,
                &prefix,
                module_info,
                file_conversion,
                lsp_position,
                &mut completions,
            );
        }
    }

    for completion in completions {
        builder.add_completion_item(completion);
    }

    Some(())
}

fn add_module_completion_item(
    builder: &CompletionBuilder,
    prefix: &str,
    module_info: &ModuleInfo,
    file_conversion: EmmyrcFilenameConvention,
    position: Position,
    completions: &mut Vec<CompletionItem>,
) -> Option<()> {
    if !check_export_visibility(&builder.semantic_model, module_info).unwrap_or(false) {
        return None;
    }

    let completion_name = module_name_convert(module_info, file_conversion);
    if !completion_name.to_lowercase().starts_with(prefix) {
        // 如果模块名不匹配, 则根据导出类型添加完成项
        add_completion_item_by_type(
            builder,
            prefix,
            module_info,
            file_conversion,
            position,
            completions,
        );
        return None;
    }

    if builder.env_duplicate_name.contains(&completion_name) {
        return None;
    }

    let data = if let Some(property_id) = &module_info.semantic_id {
        CompletionData::from_property_owner_id(builder, property_id.clone(), None)
    } else {
        None
    };
    let completion_item = CompletionItem {
        label: completion_name.clone(),
        kind: Some(lsp_types::CompletionItemKind::MODULE),
        filter_text: Some(format!(
            "{} {}",
            completion_name, module_info.full_module_name
        )),
        label_details: Some(lsp_types::CompletionItemLabelDetails {
            detail: Some(format!("    (in {})", module_info.full_module_name)),
            ..Default::default()
        }),
        command: Some(make_auto_require(
            "",
            builder.semantic_model.get_file_id(),
            module_info.file_id,
            position,
            completion_name,
            None,
        )),
        data,
        ..Default::default()
    };

    completions.push(completion_item);

    Some(())
}

fn add_completion_item_by_type(
    builder: &CompletionBuilder,
    prefix: &str,
    module_info: &ModuleInfo,
    file_conversion: EmmyrcFilenameConvention,
    position: Position,
    completions: &mut Vec<CompletionItem>,
) -> Option<()> {
    // 模块必须要有 export 标记
    module_info.get_export(builder.semantic_model.get_db())?;

    if let Some(export_type) = &module_info.export_type {
        match export_type {
            LuaType::TableConst(_) | LuaType::Def(_) => {
                let member_infos = builder.semantic_model.get_member_infos(export_type)?;
                for member_info in member_infos {
                    let key_name = file_name_convert(
                        &member_info.key.to_path(),
                        &member_info.typ,
                        file_conversion,
                    );
                    match member_info.typ {
                        LuaType::Def(_) => {}
                        LuaType::Signature(_) => {}
                        LuaType::DocFunction(_) => {}
                        LuaType::Ref(_) => {
                            let Some(LuaSemanticDeclId::Member(member_id)) =
                                member_info.property_owner_id.as_ref()
                            else {
                                continue;
                            };
                            let Some(property) = builder
                                .semantic_model
                                .get_db()
                                .get_property_index()
                                .get_property(&LuaSemanticDeclId::Member(member_id.clone()))
                            else {
                                continue;
                            };
                            // 允许标记有 export 标记的引用成员被自动导入捕获
                            if property.export().is_none() {
                                continue;
                            }
                        }
                        _ => {
                            continue;
                        }
                    }

                    if key_name.to_lowercase().starts_with(prefix) {
                        if builder.env_duplicate_name.contains(&key_name) {
                            continue;
                        }

                        let data = if let Some(property_owner_id) = &member_info.property_owner_id {
                            let is_visible = builder.semantic_model.is_semantic_visible(
                                builder.trigger_token.clone(),
                                property_owner_id.clone(),
                            );
                            if !is_visible {
                                continue;
                            }
                            CompletionData::from_property_owner_id(
                                builder,
                                property_owner_id.clone(),
                                None,
                            )
                        } else {
                            None
                        };

                        let completion_item = CompletionItem {
                            label: key_name.clone(),
                            kind: Some(get_completion_kind(&member_info.typ)),
                            filter_text: Some(format!(
                                "{} {} {}",
                                key_name,
                                module_info.full_module_name,
                                member_info.key.to_path()
                            )),
                            label_details: Some(lsp_types::CompletionItemLabelDetails {
                                detail: Some(format!("    (in {})", module_info.full_module_name)),
                                ..Default::default()
                            }),
                            command: Some(make_auto_require(
                                "",
                                builder.semantic_model.get_file_id(),
                                module_info.file_id,
                                position,
                                key_name,
                                Some(member_info.key.to_path().to_string()),
                            )),
                            data,
                            ..Default::default()
                        };

                        completions.push(completion_item);
                    }
                }
            }
            LuaType::Signature(_) => {
                let semantic_id = module_info.semantic_id.as_ref()?;
                if let LuaSemanticDeclId::LuaDecl(decl_id) = semantic_id {
                    let decl = builder
                        .semantic_model
                        .get_db()
                        .get_decl_index()
                        .get_decl(&decl_id)?;
                    let name = decl.get_name();
                    if name.to_lowercase().starts_with(prefix) {
                        if builder.env_duplicate_name.contains(name) {
                            return None;
                        }

                        let completion_item = CompletionItem {
                            label: name.to_string(),
                            kind: Some(get_completion_kind(&export_type)),
                            filter_text: Some(format!("{} {}", name, module_info.full_module_name)),
                            label_details: Some(lsp_types::CompletionItemLabelDetails {
                                detail: Some(format!("    (in {})", module_info.full_module_name)),
                                ..Default::default()
                            }),
                            command: Some(make_auto_require(
                                "",
                                builder.semantic_model.get_file_id(),
                                module_info.file_id,
                                position,
                                name.to_string(),
                                None,
                            )),
                            ..Default::default()
                        };

                        completions.push(completion_item);
                    }
                }
            }
            _ => {}
        }
    }
    Some(())
}