use std::str::FromStr;
use glua_code_analysis::{
LuaCompilation, LuaDeclId, LuaMemberId, LuaMemberInfo, LuaMemberKey, LuaMemberOwner,
LuaSemanticDeclId, LuaSignatureId, LuaType, LuaTypeDeclId, SemanticDeclLevel, SemanticModel,
};
use glua_parser::{
LuaAstNode, LuaAstToken, LuaCallExpr, LuaExpr, LuaFuncStat, LuaIndexExpr, LuaLocalFuncStat,
LuaReturnStat, LuaStringToken, LuaSyntaxKind, LuaSyntaxToken, LuaTableExpr, LuaTableField,
LuaVarExpr,
};
use itertools::Itertools;
use lsp_types::{GotoDefinitionResponse, Location, Position, Range, Uri};
use crate::{
handlers::{
definition::goto_function::{
find_function_call_origin, find_matching_function_definitions,
},
hover::{DeclOriginResult, find_all_same_named_members, find_member_origin_owners},
},
util::{to_camel_case, to_pascal_case, to_snake_case},
};
pub fn goto_def_definition(
semantic_model: &SemanticModel,
compilation: &LuaCompilation,
semantic_id: LuaSemanticDeclId,
trigger_token: &LuaSyntaxToken,
) -> Option<GotoDefinitionResponse> {
if let Some(property) = semantic_model
.get_db()
.get_property_index()
.get_property(&semantic_id)
&& let Some(source) = property.source()
&& let Some(location) = goto_source_location(source)
{
return Some(GotoDefinitionResponse::Scalar(location));
}
match &semantic_id {
LuaSemanticDeclId::LuaDecl(decl_id) => handle_decl_definition(
semantic_model,
compilation,
trigger_token,
&semantic_id,
decl_id,
),
LuaSemanticDeclId::Member(member_id) => {
handle_member_definition(semantic_model, compilation, trigger_token, member_id)
}
LuaSemanticDeclId::TypeDecl(type_decl_id) => {
handle_type_decl_definition(semantic_model, type_decl_id)
}
_ => None,
}
}
fn handle_decl_definition(
semantic_model: &SemanticModel,
compilation: &LuaCompilation,
trigger_token: &LuaSyntaxToken,
property_owner: &LuaSemanticDeclId,
decl_id: &LuaDeclId,
) -> Option<GotoDefinitionResponse> {
if let Some(match_semantic_decl) =
find_function_call_origin(semantic_model, compilation, trigger_token, property_owner)
&& let LuaSemanticDeclId::LuaDecl(matched_decl_id) = match_semantic_decl
{
if let Some(signature_id) = trigger_token_signature_id(semantic_model, trigger_token)
&& signature_id.get_file_id() == decl_id.file_id
&& let Some(location) = get_signature_definition_location(semantic_model, &signature_id)
{
return Some(GotoDefinitionResponse::Scalar(location));
}
if let Some(location) = get_decl_location(semantic_model, &matched_decl_id) {
return Some(GotoDefinitionResponse::Scalar(location));
}
}
if let Some(signature_id) = trigger_token_signature_id(semantic_model, trigger_token)
&& signature_id.get_file_id() == decl_id.file_id
&& let Some(location) = get_signature_definition_location(semantic_model, &signature_id)
{
return Some(GotoDefinitionResponse::Scalar(location));
}
if let Some(location) = get_decl_location(semantic_model, decl_id) {
return Some(GotoDefinitionResponse::Scalar(location));
}
if decl_id.file_id != semantic_model.get_file_id()
&& let Some(semantic_decl) =
semantic_model.find_decl(trigger_token.clone().into(), SemanticDeclLevel::NoTrace)
&& let LuaSemanticDeclId::LuaDecl(decl_id) = semantic_decl
&& let Some(location) = get_decl_location(semantic_model, &decl_id)
{
return Some(GotoDefinitionResponse::Scalar(location));
}
None
}
fn trigger_token_signature_id(
semantic_model: &SemanticModel,
trigger_token: &LuaSyntaxToken,
) -> Option<LuaSignatureId> {
match semantic_model
.get_semantic_info(trigger_token.clone().into())?
.typ
{
LuaType::Signature(signature_id) => Some(signature_id),
_ => None,
}
}
fn get_signature_definition_location(
semantic_model: &SemanticModel,
signature_id: &LuaSignatureId,
) -> Option<Location> {
let root = semantic_model.get_root_by_file_id(signature_id.get_file_id())?;
let token = match root.syntax().token_at_offset(signature_id.get_position()) {
rowan::TokenAtOffset::Single(token) => token,
rowan::TokenAtOffset::Between(left, _) => left,
rowan::TokenAtOffset::None => return None,
};
for ancestor in token.parent_ancestors() {
if let Some(func_stat) = LuaFuncStat::cast(ancestor.clone()) {
let func_name = func_stat.get_func_name()?;
let range = match func_name {
LuaVarExpr::NameExpr(name_expr) => name_expr.get_name_token()?.get_range(),
LuaVarExpr::IndexExpr(index_expr) => index_expr
.get_index_name_token()
.map(|token| token.text_range())
.unwrap_or_else(|| index_expr.get_range()),
};
let document = semantic_model.get_document_by_file_id(signature_id.get_file_id())?;
return document.to_lsp_location(range);
}
if let Some(local_func_stat) = LuaLocalFuncStat::cast(ancestor) {
let local_name = local_func_stat.get_local_name()?;
let document = semantic_model.get_document_by_file_id(signature_id.get_file_id())?;
return document.to_lsp_location(local_name.get_range());
}
}
None
}
fn handle_member_definition(
semantic_model: &SemanticModel,
compilation: &LuaCompilation,
trigger_token: &LuaSyntaxToken,
member_id: &LuaMemberId,
) -> Option<GotoDefinitionResponse> {
let same_named_members = find_all_same_named_members(
semantic_model,
&Some(LuaSemanticDeclId::Member(*member_id)),
Some(trigger_token.text_range().start()),
)?;
let mut locations: Vec<Location> = Vec::new();
if let Some(match_members) = find_matching_function_definitions(
semantic_model,
compilation,
trigger_token,
&same_named_members,
) {
process_matched_members(semantic_model, compilation, &match_members, &mut locations);
if !locations.is_empty() {
return Some(GotoDefinitionResponse::Array(locations));
}
}
let instance_table_locations =
collect_instance_table_member_locations(semantic_model, trigger_token, member_id);
let should_skip_current_index_member = !instance_table_locations.is_empty()
&& trigger_token
.parent()
.is_some_and(|node| LuaIndexExpr::can_cast(node.kind().into()));
for member in same_named_members {
if let LuaSemanticDeclId::Member(candidate_member_id) = member
&& let Some(location) = get_member_location(semantic_model, &candidate_member_id)
{
if should_skip_current_index_member && candidate_member_id == *member_id {
continue;
}
try_set_accessor_locations(
semantic_model,
&member,
&mut locations,
trigger_token.text_range().start(),
);
locations.push(location);
}
}
locations.extend(instance_table_locations);
add_member_origin_locations(semantic_model, compilation, member_id, &mut locations);
if !locations.is_empty() {
Some(GotoDefinitionResponse::Array(
locations.into_iter().unique().collect(),
))
} else {
None
}
}
fn handle_type_decl_definition(
semantic_model: &SemanticModel,
type_decl_id: &LuaTypeDeclId,
) -> Option<GotoDefinitionResponse> {
let type_decl = semantic_model
.get_db()
.get_type_index()
.get_type_decl(type_decl_id)?;
let mut locations: Vec<Location> = Vec::new();
for lua_location in type_decl.get_locations() {
let document = semantic_model.get_document_by_file_id(lua_location.file_id)?;
let location = document.to_lsp_location(lua_location.range)?;
locations.push(location);
}
Some(GotoDefinitionResponse::Array(locations))
}
fn process_matched_members(
semantic_model: &SemanticModel,
compilation: &LuaCompilation,
match_members: &[LuaSemanticDeclId],
locations: &mut Vec<Location>,
) {
for member in match_members {
match member {
LuaSemanticDeclId::Member(member_id) => {
if should_trace_member(semantic_model, member_id).unwrap_or(false) {
match find_member_origin_owners(
compilation,
semantic_model,
*member_id,
false,
None,
)
.get_first()
{
Some(LuaSemanticDeclId::Member(origin_member_id)) => {
if let Some(location) =
get_member_location(semantic_model, &origin_member_id)
{
locations.push(location);
continue;
}
}
Some(LuaSemanticDeclId::LuaDecl(origin_decl_id)) => {
if let Some(location) =
get_decl_location(semantic_model, &origin_decl_id)
{
locations.push(location);
continue;
}
}
_ => {}
}
}
if let Some(location) = get_member_location(semantic_model, member_id) {
locations.push(location);
}
}
LuaSemanticDeclId::LuaDecl(decl_id) => {
if let Some(location) = get_decl_location(semantic_model, decl_id) {
locations.push(location);
}
}
_ => {}
}
}
}
fn collect_instance_table_member_locations(
semantic_model: &SemanticModel,
trigger_token: &LuaSyntaxToken,
member_id: &LuaMemberId,
) -> Vec<Location> {
let mut locations = Vec::new();
if let Some(table_field_infos) =
find_instance_table_member(semantic_model, trigger_token, member_id)
{
for table_field_info in table_field_infos {
if let Some(LuaSemanticDeclId::Member(table_member_id)) =
table_field_info.property_owner_id
&& let Some(location) = get_member_location(semantic_model, &table_member_id)
{
locations.push(location);
}
}
}
locations
}
fn add_member_origin_locations(
semantic_model: &SemanticModel,
compilation: &LuaCompilation,
member_id: &LuaMemberId,
locations: &mut Vec<Location>,
) {
let origin_result =
find_member_origin_owners(compilation, semantic_model, *member_id, true, None);
match origin_result {
DeclOriginResult::Single(LuaSemanticDeclId::Member(origin_id)) => {
if origin_id != *member_id {
if let Some(location) = get_member_location(semantic_model, &origin_id) {
locations.push(location);
}
}
}
DeclOriginResult::Multiple(members) => {
for member in members {
if let LuaSemanticDeclId::Member(origin_id) = member {
if origin_id != *member_id {
if let Some(location) = get_member_location(semantic_model, &origin_id) {
locations.push(location);
}
}
}
}
}
_ => {}
}
}
fn goto_source_location(source: &str) -> Option<Location> {
let source = source.trim();
if is_web_source_uri(source) {
return None;
}
if let Some((uri, range_text)) = source.rsplit_once('#')
&& let Some(range) = parse_source_range(range_text)
{
return Some(Location {
uri: Uri::from_str(uri).ok()?,
range,
});
}
Some(Location {
uri: Uri::from_str(source).ok()?,
range: Range {
start: Position::new(0, 0),
end: Position::new(0, 0),
},
})
}
fn is_web_source_uri(source: &str) -> bool {
let Some((scheme, _)) = source.split_once(':') else {
return false;
};
scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
}
fn parse_source_range(range_text: &str) -> Option<Range> {
let (mut line_text, col_text) = range_text.split_once(':')?;
if line_text.to_ascii_lowercase().starts_with('l') {
line_text = &line_text[1..];
}
let line = line_text.parse::<u32>().ok()?;
let col = col_text.parse::<u32>().ok()?;
Some(Range {
start: Position::new(line, col),
end: Position::new(line, col),
})
}
pub fn goto_str_tpl_ref_definition(
semantic_model: &SemanticModel,
string_token: LuaStringToken,
) -> Option<GotoDefinitionResponse> {
let name = string_token.get_value();
let call_expr = string_token.ancestors::<LuaCallExpr>().next()?;
let arg_exprs = call_expr.get_args_list()?.get_args().collect::<Vec<_>>();
let string_token_idx = arg_exprs.iter().position(|arg| {
if let LuaExpr::LiteralExpr(literal_expr) = arg {
literal_expr
.syntax()
.text_range()
.contains(string_token.get_range().start())
} else {
false
}
})?;
let func = semantic_model.infer_call_expr_func(call_expr.clone(), None)?;
let params = func.get_params();
let target_param = match (func.is_colon_define(), call_expr.is_colon_call()) {
(false, true) => params.get(string_token_idx + 1),
(true, false) => {
if string_token_idx > 0 {
params.get(string_token_idx - 1)
} else {
None
}
}
_ => params.get(string_token_idx),
}?;
if let Some(locations) =
try_extract_str_tpl_ref_locations(semantic_model, &target_param.1, &name)
{
return Some(GotoDefinitionResponse::Array(locations));
}
if let Some(LuaType::Union(union_type)) = target_param.1.clone() {
for union_member in union_type.into_vec().iter() {
if let Some(locations) = try_extract_str_tpl_ref_locations(
semantic_model,
&Some(union_member.clone()),
&name,
) {
return Some(GotoDefinitionResponse::Array(locations));
}
}
}
None
}
pub fn find_instance_table_member(
semantic_model: &SemanticModel,
trigger_token: &LuaSyntaxToken,
member_id: &LuaMemberId,
) -> Option<Vec<LuaMemberInfo>> {
let member_key = semantic_model
.get_db()
.get_member_index()
.get_member(member_id)?
.get_key();
let parent = trigger_token.parent()?;
match parent {
expr_node if LuaIndexExpr::can_cast(expr_node.kind().into()) => {
let index_expr = LuaIndexExpr::cast(expr_node)?;
let prefix_expr = index_expr.get_prefix_expr()?;
let decl = semantic_model.find_decl(
prefix_expr.syntax().clone().into(),
SemanticDeclLevel::default(),
);
if let Some(LuaSemanticDeclId::LuaDecl(decl_id)) = decl {
return find_member_in_table_const(
semantic_model,
&decl_id,
member_key,
trigger_token.text_range().start(),
);
}
}
table_field_node if LuaTableField::can_cast(table_field_node.kind().into()) => {
let table_field = LuaTableField::cast(table_field_node)?;
let table_expr = table_field.get_parent::<LuaTableExpr>()?;
let typ = semantic_model.infer_table_should_be(table_expr)?;
return semantic_model.get_member_info_with_key_at_offset(
&typ,
member_key.clone(),
true,
trigger_token.text_range().start(),
);
}
_ => {}
}
None
}
fn find_member_in_table_const(
semantic_model: &SemanticModel,
decl_id: &LuaDeclId,
member_key: &LuaMemberKey,
position_offset: rowan::TextSize,
) -> Option<Vec<LuaMemberInfo>> {
let root = semantic_model
.get_db()
.get_vfs()
.get_syntax_tree(&decl_id.file_id)?
.get_red_root();
let node = semantic_model
.get_db()
.get_decl_index()
.get_decl(decl_id)?
.get_value_syntax_id()?
.to_node_from_root(&root)?;
let table_expr = LuaTableExpr::cast(node)?;
let typ = semantic_model
.infer_expr(LuaExpr::TableExpr(table_expr))
.ok()?;
semantic_model.get_member_info_with_key_at_offset(
&typ,
member_key.clone(),
true,
position_offset,
)
}
fn should_trace_member(semantic_model: &SemanticModel, member_id: &LuaMemberId) -> Option<bool> {
let root = semantic_model
.get_db()
.get_vfs()
.get_syntax_tree(&member_id.file_id)?
.get_red_root();
let node = member_id.get_syntax_id().to_node_from_root(&root)?;
let parent = node.parent()?.parent()?;
if LuaReturnStat::can_cast(parent.kind().into()) {
return Some(true);
} else {
let typ = semantic_model.get_type((*member_id).into());
if typ.is_signature() {
return Some(true);
}
}
None
}
fn get_member_location(
semantic_model: &SemanticModel,
member_id: &LuaMemberId,
) -> Option<Location> {
let document = semantic_model.get_document_by_file_id(member_id.file_id)?;
let range = member_id.get_syntax_id().get_range();
if member_id.get_syntax_id().get_kind() == LuaSyntaxKind::IndexExpr {
if let Some(key_range) = resolve_index_expr_key_range(semantic_model, member_id) {
return document.to_lsp_location(key_range);
}
}
document.to_lsp_location(range)
}
fn resolve_index_expr_key_range(
semantic_model: &SemanticModel,
member_id: &LuaMemberId,
) -> Option<rowan::TextRange> {
let root = semantic_model
.get_db()
.get_vfs()
.get_syntax_tree(&member_id.file_id)?
.get_red_root();
let node = member_id.get_syntax_id().to_node_from_root(&root)?;
let index_expr = LuaIndexExpr::cast(node)?;
index_expr.get_index_key()?.get_range()
}
fn get_decl_location(semantic_model: &SemanticModel, decl_id: &LuaDeclId) -> Option<Location> {
let decl = semantic_model.get_db().get_decl_index().get_decl(decl_id)?;
let document = semantic_model.get_document_by_file_id(decl_id.file_id)?;
let location = document.to_lsp_location(decl.get_range())?;
Some(location)
}
fn try_extract_str_tpl_ref_locations(
semantic_model: &SemanticModel,
param_type: &Option<LuaType>,
name: &str,
) -> Option<Vec<Location>> {
if let Some(LuaType::StrTplRef(str_tpl)) = param_type {
let prefix = str_tpl.get_prefix();
let suffix = str_tpl.get_suffix();
let type_decl_id = LuaTypeDeclId::global(format!("{}{}{}", prefix, name, suffix).as_str());
let type_decl = semantic_model
.get_db()
.get_type_index()
.get_type_decl(&type_decl_id)?;
let mut locations = Vec::new();
for lua_location in type_decl.get_locations() {
let document = semantic_model.get_document_by_file_id(lua_location.file_id)?;
let location = document.to_lsp_location(lua_location.range)?;
locations.push(location);
}
return Some(locations);
}
None
}
fn try_set_accessor_locations(
semantic_model: &SemanticModel,
semantic_decl_id: &LuaSemanticDeclId,
locations: &mut Vec<Location>,
position_offset: rowan::TextSize,
) -> Option<()> {
#[derive(Clone, Copy)]
enum AccessorCaseConvention {
CamelCase, SnakeCase, PascalCase, }
impl AccessorCaseConvention {
fn build_name(self, prefix: &str, field_name: &str) -> Option<String> {
if field_name.is_empty() {
return None;
}
let full_name = format!("{}_{}", prefix, field_name);
let name = match self {
AccessorCaseConvention::CamelCase => to_camel_case(&full_name),
AccessorCaseConvention::SnakeCase => to_snake_case(&full_name),
AccessorCaseConvention::PascalCase => to_pascal_case(&full_name),
};
Some(name)
}
}
let member_id = match semantic_decl_id {
LuaSemanticDeclId::Member(id) => id,
_ => return None,
};
let current_owner = semantic_model
.get_db()
.get_member_index()
.get_current_owner(member_id)?;
let prefix_type = match current_owner {
LuaMemberOwner::Type(id) => LuaType::Ref(id.clone()),
_ => return None,
};
let property = semantic_model
.get_db()
.get_property_index()
.get_property(&semantic_decl_id)?;
let attribute_use = property.find_attribute_use("field_accessor")?;
let has_getter =
if let Some(LuaType::DocStringConst(getter)) = attribute_use.get_param_by_name("getter") {
try_add_accessor_location(
semantic_model,
&prefix_type,
getter.as_str().into(),
locations,
position_offset,
)
} else {
false
};
let has_setter =
if let Some(LuaType::DocStringConst(setter)) = attribute_use.get_param_by_name("setter") {
try_add_accessor_location(
semantic_model,
&prefix_type,
setter.as_str().into(),
locations,
position_offset,
)
} else {
false
};
if has_getter && has_setter {
return Some(());
}
let convention = {
if let Some(LuaType::DocStringConst(convention)) =
attribute_use.get_param_by_name("convention")
{
match convention.as_str() {
"camelCase" => AccessorCaseConvention::CamelCase,
"snake_case" => AccessorCaseConvention::SnakeCase,
"PascalCase" => AccessorCaseConvention::PascalCase,
_ => AccessorCaseConvention::CamelCase,
}
} else {
AccessorCaseConvention::CamelCase
}
};
let Some(original_name) = semantic_model
.get_db()
.get_member_index()
.get_member(member_id)?
.get_key()
.get_name()
else {
return Some(());
};
if !has_getter {
if let Some(getter_name) = convention.build_name("get", original_name) {
try_add_accessor_location(
semantic_model,
&prefix_type,
getter_name,
locations,
position_offset,
);
}
}
if !has_setter {
if let Some(setter_name) = convention.build_name("set", original_name) {
try_add_accessor_location(
semantic_model,
&prefix_type,
setter_name,
locations,
position_offset,
);
}
}
Some(())
}
fn try_add_accessor_location(
semantic_model: &SemanticModel,
prefix_type: &LuaType,
accessor_name: String,
locations: &mut Vec<Location>,
position_offset: rowan::TextSize,
) -> bool {
let accessor_key = LuaMemberKey::Name(accessor_name.as_str().into());
if let Some(member_infos) = semantic_model.get_member_info_with_key_at_offset(
prefix_type,
accessor_key,
false,
position_offset,
) {
if let Some(member_info) = member_infos.first()
&& let Some(LuaSemanticDeclId::Member(accessor_id)) = member_info.property_owner_id
&& let Some(location) = get_member_location(semantic_model, &accessor_id)
{
locations.push(location);
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::goto_source_location;
use googletest::prelude::*;
#[gtest]
fn test_goto_source_location_supports_plain_file_uri() -> Result<()> {
let location = goto_source_location("file:///tmp/test.lua")
.ok_or("missing source location")
.or_fail()?;
verify_eq!(location.uri.as_str(), "file:///tmp/test.lua")?;
verify_eq!(location.range.start.line, 0)?;
verify_eq!(location.range.start.character, 0)?;
Ok(())
}
#[gtest]
fn test_goto_source_location_ignores_http_url() -> Result<()> {
verify_that!(
goto_source_location("https://wiki.facepunch.com/gmod/Entity:SetPos"),
none()
)
}
#[gtest]
fn test_goto_source_location_supports_uri_with_range() -> Result<()> {
let location = goto_source_location("file:///tmp/test.lua#L41:2")
.ok_or("missing source location")
.or_fail()?;
verify_eq!(location.uri.as_str(), "file:///tmp/test.lua")?;
verify_eq!(location.range.start.line, 41)?;
verify_eq!(location.range.start.character, 2)?;
Ok(())
}
}