glua_ls 1.0.27

Language server for Garry's Mod Lua (GLua).
Documentation
use glua_code_analysis::{
    DbIndex, FileId, GmodClassCallLiteral, GmodScriptedClassCallMetadata, LuaDocument,
    file_path_to_uri,
};
use tokio_util::sync::CancellationToken;

use super::gmod_scripted_classes_request::{GmodScriptedClassEntry, GmodScriptedClassesResult};

pub fn build_gmod_scripted_classes(
    db: &DbIndex,
    cancel_token: &CancellationToken,
) -> Option<GmodScriptedClassesResult> {
    if cancel_token.is_cancelled() {
        return None;
    }

    let scopes = &db.get_emmyrc().gmod.scripted_class_scopes;
    let definitions = scopes.resolved_definitions();

    let mut file_paths = Vec::new();
    for file_id in db.get_vfs().get_all_local_file_ids() {
        if cancel_token.is_cancelled() {
            return None;
        }

        if let Some(file_path) = db.get_vfs().get_file_path(&file_id) {
            file_paths.push((file_id, file_path.as_path()));
        }
    }

    let (_, scoped_matches) = scopes.scan_scripted_class_scope_files(file_paths);

    let mut entries = Vec::new();
    for (file_id, scope_match) in scoped_matches {
        if cancel_token.is_cancelled() {
            return None;
        }

        let Some(uri) = file_uri_string(db, file_id) else {
            continue;
        };

        entries.push(GmodScriptedClassEntry {
            uri,
            class_type: scope_match.definition.class_global.clone(),
            class_name: scope_match.class_name,
            definition_id: Some(scope_match.definition.id),
            range: None,
        });
    }

    for (file_id, file_metadata) in db.get_gmod_class_metadata_index().iter_file_metadata() {
        if cancel_token.is_cancelled() {
            return None;
        }

        let Some(uri) = file_uri_string(db, *file_id) else {
            continue;
        };
        let document = db.get_vfs().get_document(file_id);

        push_vgui_panel_entries(
            &mut entries,
            &uri,
            document.as_ref(),
            &file_metadata.vgui_register_calls,
        );
        push_vgui_panel_entries(
            &mut entries,
            &uri,
            document.as_ref(),
            &file_metadata.derma_define_control_calls,
        );
    }

    entries.sort_by(|left, right| {
        left.class_type
            .cmp(&right.class_type)
            .then_with(|| left.class_name.cmp(&right.class_name))
            .then_with(|| left.uri.cmp(&right.uri))
    });
    entries.dedup_by(|left, right| {
        left.uri == right.uri
            && left.class_type == right.class_type
            && left.class_name == right.class_name
    });

    Some(GmodScriptedClassesResult {
        definitions,
        entries,
    })
}

fn file_uri_string(db: &DbIndex, file_id: FileId) -> Option<String> {
    db.get_vfs()
        .get_uri(&file_id)
        .or_else(|| {
            db.get_vfs()
                .get_file_path(&file_id)
                .and_then(|file_path| file_path_to_uri(&file_path))
        })
        .map(|uri| uri.to_string())
}

fn push_vgui_panel_entries(
    entries: &mut Vec<GmodScriptedClassEntry>,
    uri: &str,
    document: Option<&LuaDocument<'_>>,
    calls: &[GmodScriptedClassCallMetadata],
) {
    for call in calls {
        let Some(panel_name) = extract_vgui_panel_name(call) else {
            continue;
        };
        let range = document.and_then(|doc| doc.to_lsp_range(call.syntax_id.get_range()));

        entries.push(GmodScriptedClassEntry {
            uri: uri.to_string(),
            class_type: "VGUI".to_string(),
            class_name: panel_name.to_string(),
            definition_id: None,
            range,
        });
    }
}

fn extract_vgui_panel_name(call: &GmodScriptedClassCallMetadata) -> Option<&str> {
    match call.literal_args.first() {
        Some(Some(GmodClassCallLiteral::String(name))) if !name.is_empty() => Some(name.as_str()),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use glua_code_analysis::{Emmyrc, VirtualWorkspace};
    use googletest::prelude::*;
    use tokio_util::sync::CancellationToken;

    use super::build_gmod_scripted_classes;

    #[gtest]
    fn build_gmod_scripted_classes_filters_to_scoped_paths() -> Result<()> {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file("lua/entities/test_entity/init.lua", "local ENT = {}");
        ws.def_file("lua/plugins/my_plugin/sh_init.lua", "local PLUGIN = {}");
        ws.def_file("lua/autorun/ignored.lua", "local x = 1");

        let entries = build_gmod_scripted_classes(ws.get_db_mut(), &CancellationToken::new())
            .or_fail()?
            .entries;

        verify_that!(
            entries
                .iter()
                .any(|entry| entry.class_type == "ENT" && entry.class_name == "test_entity"),
            eq(true)
        )?;
        verify_that!(
            entries
                .iter()
                .any(|entry| entry.class_type == "PLUGIN" && entry.class_name == "my_plugin"),
            eq(true)
        )?;
        verify_that!(
            entries.iter().any(|entry| {
                entry.class_type == "ENT"
                    && entry.class_name == "test_entity"
                    && entry.range.is_none()
            }),
            eq(true)
        )?;
        verify_that!(
            entries.iter().any(|entry| entry.class_name == "ignored"),
            eq(false)
        )
    }

    #[gtest]
    fn build_gmod_scripted_classes_uses_per_definition_excludes_for_stools() -> Result<()> {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/weapons/gmod_tool/stools/hoverball.lua",
            "local TOOL = {}",
        );

        let entries = build_gmod_scripted_classes(ws.get_db_mut(), &CancellationToken::new())
            .or_fail()?
            .entries;

        verify_that!(
            entries.iter().any(|entry| {
                entry.class_type == "TOOL"
                    && entry.class_name == "hoverball"
                    && entry.definition_id.as_deref() == Some("stools")
            }),
            eq(true)
        )
    }

    #[gtest]
    fn build_gmod_scripted_classes_includes_vgui_panels_from_metadata() -> Result<()> {
        let mut ws = VirtualWorkspace::new();
        let mut emmyrc = Emmyrc::default();
        emmyrc.gmod.enabled = true;
        ws.update_emmyrc(emmyrc);

        ws.def_file(
            "lua/autorun/client/cl_panel_defs.lua",
            r#"
            vgui.Register("MyPanel", {}, "DPanel")
            derma.DefineControl("MyControl", "desc", {}, "DPanel")

            local panel_name = "DynamicPanel"
            vgui.Register(panel_name, {}, "DFrame")
            derma.DefineControl("", "desc", {}, "DLabel")
        "#,
        );

        let entries = build_gmod_scripted_classes(ws.get_db_mut(), &CancellationToken::new())
            .or_fail()?
            .entries;

        verify_that!(
            entries
                .iter()
                .any(|entry| { entry.class_type == "VGUI" && entry.class_name == "MyPanel" }),
            eq(true)
        )?;
        verify_that!(
            entries
                .iter()
                .any(|entry| { entry.class_type == "VGUI" && entry.class_name == "MyControl" }),
            eq(true)
        )?;
        verify_that!(
            entries.iter().any(|entry| {
                entry.class_type == "VGUI" && entry.class_name == "MyPanel" && entry.range.is_some()
            }),
            eq(true)
        )?;
        verify_that!(
            entries
                .iter()
                .any(|entry| { entry.class_type == "VGUI" && entry.class_name == "DynamicPanel" }),
            eq(false)
        )?;
        verify_that!(
            entries
                .iter()
                .any(|entry| entry.class_type == "VGUI" && entry.class_name.is_empty()),
            eq(false)
        )
    }
}