use lsp_types::request::Request;
use lsp_types::{Position, TextDocumentIdentifier};
use serde::{Deserialize, Serialize};
use glua_code_analysis::{DEFAULT_DETAIL_MEMBER_DISPLAY_COUNT, LuaType, SemanticModel};
use glua_parser::LuaAstNode;
use rowan::TokenAtOffset;
#[derive(Debug)]
pub enum GluaHoverExpandRequest {}
impl Request for GluaHoverExpandRequest {
type Params = HoverExpandParams;
type Result = Option<HoverExpandResponse>;
const METHOD: &'static str = "gluals/hoverExpand";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HoverExpandParams {
pub text_document: TextDocumentIdentifier,
pub position: Position,
pub level: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HoverExpandResponse {
pub content: lsp_types::HoverContents,
pub range: Option<lsp_types::Range>,
pub max_level: u32,
}
pub fn level_to_display_count(level: u32) -> usize {
match level {
0 => DEFAULT_DETAIL_MEMBER_DISPLAY_COUNT, 1 => 12, 2 => 24, 3 => 50, 4 => 100, _ => usize::MAX, }
}
pub fn compute_max_level(total: usize) -> u32 {
(0..)
.find(|level| level_to_display_count(*level) >= total)
.unwrap_or(0)
}
fn compute_max_level_for_type(semantic_model: &SemanticModel, typ: &LuaType) -> u32 {
let total = match typ {
LuaType::Def(_)
| LuaType::Ref(_)
| LuaType::TableConst(_)
| LuaType::MergedTable(_)
| LuaType::Instance(_) => semantic_model
.get_member_infos(typ)
.map(|members| members.len())
.unwrap_or(0),
LuaType::Object(object) => object.get_fields().len(),
_ => 0,
};
compute_max_level(total)
}
pub fn compute_max_level_at_position(semantic_model: &SemanticModel, position: Position) -> u32 {
let document = semantic_model.get_document();
let Some(offset) = document.get_offset(position.line as usize, position.character as usize)
else {
return 0;
};
let root = semantic_model.get_root();
let token = match root.syntax().token_at_offset(offset) {
TokenAtOffset::Single(t) => t,
TokenAtOffset::Between(l, _) => l,
TokenAtOffset::None => return 0,
};
let semantic_info = semantic_model.get_semantic_info(token.into());
let typ = match semantic_info {
Some(info) => info.typ,
None => return 0,
};
compute_max_level_for_type(semantic_model, &typ)
}
#[cfg(test)]
mod tests {
use crate::handlers::test_lib::check;
use glua_code_analysis::{LuaType, LuaTypeDeclId, RenderLevel, VirtualWorkspace};
use googletest::prelude::*;
use lsp_types::HoverContents;
use rowan::TextSize;
use super::{
compute_max_level, compute_max_level_at_position, compute_max_level_for_type,
level_to_display_count,
};
#[gtest]
fn max_level_display_count_covers_large_member_count() {
let total = 501;
let max_level = compute_max_level(total);
assert_that!(level_to_display_count(max_level), ge(total));
}
#[gtest]
fn default_level_display_count_is_compact() {
assert_that!(level_to_display_count(0), eq(6));
assert_that!(compute_max_level(6), eq(0));
assert_that!(compute_max_level(7), eq(1));
}
#[gtest]
fn def_type_hover_reports_expandable_max_level() -> Result<()> {
let mut ws = VirtualWorkspace::new();
let file_id = ws.def_file(
"lua/entities/base_glide.lua",
r#"
---@class base_glide
---@field crosshair table?
---@field _playerListExpanded any
---@field weaponSwitchNotification table?
---@field _expandTimer number
---@field AutomaticFrameAdvance boolean
---@field _hasBothSpriteBeams boolean?
---@field EnableCrosshair function
"#,
);
let semantic_model = check!(ws.analysis.compilation.get_semantic_model(file_id));
assert_that!(
compute_max_level_for_type(
&semantic_model,
&LuaType::Def(LuaTypeDeclId::global("base_glide")),
),
eq(1)
);
Ok(())
}
#[gtest]
fn table_const_field_hover_reports_expandable_max_level() -> Result<()> {
let mut ws = VirtualWorkspace::new();
let content = r#"
local ENT = {}
ENT.SuspensionPoseParameters = {
{ parameter = "vehicle_wheel_fl_height", wheel = 1 },
{ parameter = "vehicle_wheel_fr_height", wheel = 2 },
{ parameter = "vehicle_wheel_rl_height", wheel = 3 },
{ parameter = "vehicle_wheel_rr_height", wheel = 4 },
{ parameter = "vehicle_wheel_fl_height", wheel = 1 },
{ parameter = "vehicle_wheel_fr_height", wheel = 2 },
{ parameter = "vehicle_wheel_rl_height", wheel = 3 },
{ parameter = "vehicle_wheel_rr_height", wheel = 4 },
{ parameter = "vehicle_wheel_fl_height", wheel = 1 },
{ parameter = "vehicle_wheel_fr_height", wheel = 2 },
{ parameter = "vehicle_wheel_rl_height", wheel = 3 },
{ parameter = "vehicle_wheel_rr_height", wheel = 4 },
{ parameter = "vehicle_wheel_fl_height", wheel = 1 },
}
"#;
let file_id = ws.def_file("lua/entities/fl_audi_r8.lua", content);
let semantic_model = check!(ws.analysis.compilation.get_semantic_model(file_id));
let document = semantic_model.get_document();
let name_offset = check!(content.find("SuspensionPoseParameters"));
let position = check!(document.to_lsp_position(TextSize::new(name_offset as u32)));
assert_that!(
compute_max_level_at_position(&semantic_model, position),
eq(2)
);
Ok(())
}
#[gtest]
fn default_table_const_field_hover_renders_six_rows() -> Result<()> {
let mut ws = VirtualWorkspace::new();
let content = r#"
local ENT = {}
ENT.SuspensionPoseParameters = {
{ parameter = "vehicle_wheel_fl_height", wheel = 1 },
{ parameter = "vehicle_wheel_fr_height", wheel = 2 },
{ parameter = "vehicle_wheel_rl_height", wheel = 3 },
{ parameter = "vehicle_wheel_rr_height", wheel = 4 },
{ parameter = "vehicle_wheel_fl_height", wheel = 1 },
{ parameter = "vehicle_wheel_fr_height", wheel = 2 },
{ parameter = "vehicle_wheel_rl_height", wheel = 3 },
}
"#;
let file_id = ws.def_file("lua/entities/fl_audi_r8.lua", content);
let semantic_model = check!(ws.analysis.compilation.get_semantic_model(file_id));
let document = semantic_model.get_document();
let name_offset = check!(content.find("SuspensionPoseParameters"));
let position = check!(document.to_lsp_position(TextSize::new(name_offset as u32)));
let hover = check!(crate::handlers::hover::hover(
&ws.analysis,
file_id,
position,
None,
));
let HoverContents::Markup(markup) = hover.contents else {
return fail!("expected HoverContents::Markup");
};
verify_that!(markup.value.as_str(), contains_substring("[6]"))?;
verify_that!(markup.value.as_str(), not(contains_substring("[7]")))?;
verify_that!(markup.value.as_str(), contains_substring(" ..."))?;
Ok(())
}
#[gtest]
fn expanded_table_const_field_hover_renders_more_rows() -> Result<()> {
let mut ws = VirtualWorkspace::new();
let content = r#"
local ENT = {}
ENT.SuspensionPoseParameters = {
{ parameter = "vehicle_wheel_fl_height", wheel = 1 },
{ parameter = "vehicle_wheel_fr_height", wheel = 2 },
{ parameter = "vehicle_wheel_rl_height", wheel = 3 },
{ parameter = "vehicle_wheel_rr_height", wheel = 4 },
{ parameter = "vehicle_wheel_fl_height", wheel = 1 },
{ parameter = "vehicle_wheel_fr_height", wheel = 2 },
{ parameter = "vehicle_wheel_rl_height", wheel = 3 },
{ parameter = "vehicle_wheel_rr_height", wheel = 4 },
{ parameter = "vehicle_wheel_fl_height", wheel = 1 },
{ parameter = "vehicle_wheel_fr_height", wheel = 2 },
{ parameter = "vehicle_wheel_rl_height", wheel = 3 },
{ parameter = "vehicle_wheel_rr_height", wheel = 4 },
{ parameter = "vehicle_wheel_fl_height", wheel = 1 },
}
"#;
let file_id = ws.def_file("lua/entities/fl_audi_r8.lua", content);
let semantic_model = check!(ws.analysis.compilation.get_semantic_model(file_id));
let document = semantic_model.get_document();
let name_offset = check!(content.find("SuspensionPoseParameters"));
let position = check!(document.to_lsp_position(TextSize::new(name_offset as u32)));
let hover = check!(crate::handlers::hover::hover(
&ws.analysis,
file_id,
position,
Some(RenderLevel::DetailedCount(24)),
));
let HoverContents::Markup(markup) = hover.contents else {
return fail!("expected HoverContents::Markup");
};
verify_that!(markup.value.as_str(), contains_substring("[13]"))?;
verify_that!(markup.value.as_str(), not(contains_substring(" ...")))?;
Ok(())
}
}