fix: improve member access hover resolution

- Fix chain detection to properly extract #var and member path
- Check all type sources (doc.types, external_types, builtins) for resolution
- Return helpful error messages when type resolution fails
- Prevent fallthrough to type check when member access is detected
- Show which type couldn't be found in resolution chain
This commit is contained in:
2026-02-21 00:32:55 +01:00
parent 1606685a20
commit d56ea306af
+81 -58
View File
@@ -314,9 +314,9 @@ function handlers.textDocument_hover(params)
local col = params.position.character + 1
-- Helper to resolve member path and get member info with comments
local function resolve_member_path(root_type, member_path, types)
local function resolve_member_path(root_type, member_path, types, external_types_lookup)
if not root_type or not member_path or member_path == "" then
return nil
return nil, nil
end
local parts = {}
@@ -325,53 +325,52 @@ function handlers.textDocument_hover(params)
end
if #parts == 0 then
return nil
return nil, nil
end
local current_type = root_type:gsub('"', '')
local current_member = nil
for i, part in ipairs(parts) do
local type_info = types and types[current_type]
-- Get type info from combined sources
local type_info = types[current_type]
-- Also check external types if not found
if not type_info and external_types_lookup then
type_info = external_types_lookup[current_type]
end
-- Try built-in types as fallback
if not type_info then
-- Try built-in types
local builtin_params = builtin.get_parameters(current_type)
if builtin_params then
local fields = builtin_params.inputs or {}
for _, field in ipairs(fields) do
if field.name == part then
current_member = field
current_type = field.type and field.type:gsub('"', '') or current_type
break
end
end
if current_member then
-- Continue to next part
else
return nil
end
else
return nil
end
else
local found = false
local fields = type_info.fields or type_info.inputs or {}
for _, field in ipairs(fields) do
if field.name == part then
current_member = field
current_type = field.type and field.type:gsub('"', '') or current_type
found = true
break
end
type_info = { fields = builtin_params.inputs or {}, inputs = builtin_params.inputs or {}, outputs = builtin_params.outputs or {} }
end
end
if not type_info then
return nil, current_type
end
if not found then
return nil
local found = false
local fields = type_info.fields or type_info.inputs or {}
for _, field in ipairs(fields) do
if field.name == part then
current_member = field
if field.type then
current_type = field.type:gsub('"', '')
end
found = true
break
end
end
if not found then
return nil, current_type
end
end
return current_member
return current_member, current_type
end
-- Check if we're in a member access chain (e.g., #var.member1.member2)
@@ -400,32 +399,48 @@ function handlers.textDocument_hover(params)
-- Check if this is a member access (contains .)
if not chain:match("%.") then
return nil, nil, chain_start, chain_end
end
-- Parse the chain: #var.member1.member2
local root_var = chain:match("#([%w_]+)")
if not root_var then
-- Try without # (for global DB access)
root_var = chain:match("^([%w_]+)%.")
return nil, nil
end
-- Parse the chain: #var.member1.member2 or var.member1.member2
local root_var = nil
local member_path = nil
-- Try to find #var pattern anywhere in the chain
root_var = chain:match("#([%w_]+)")
if root_var then
local member_path = chain:match("%.(.+)$")
return root_var, member_path, chain_start, chain_end
-- Find everything after the first #var.
member_path = chain:match("#" .. root_var .. "%.(.+)$")
end
-- If no # found, try global DB access (starts with identifier)
if not root_var then
root_var = chain:match("^([%w_]+)%.")
if root_var then
member_path = chain:match("%.(.+)$")
end
end
return nil, nil, chain_start, chain_end
if root_var and member_path then
return root_var, member_path
end
return nil, nil
end
-- Try member access first
local root_var, member_path, chain_start, chain_end = find_member_access_chain()
if root_var and member_path and doc.variables and doc.variables[root_var] then
local var_info = doc.variables[root_var]
local root_type = var_info.data_type
if root_type then
root_type = root_type:gsub('"', '')
local root_var, member_path = find_member_access_chain()
-- If we have a member access chain, try to resolve it
if root_var and member_path then
-- Check if it's a local variable (with #) or global
local var_info = nil
if doc.variables and doc.variables[root_var] then
var_info = doc.variables[root_var]
end
if var_info and var_info.data_type then
local root_type = var_info.data_type:gsub('"', '')
local all_types = {}
-- Combine all type sources
@@ -436,13 +451,8 @@ function handlers.textDocument_hover(params)
end
local external_types = plc_json.get_types(doc.root_dir or "")
for k, v in pairs(external_types) do
if not all_types[k] then
all_types[k] = v
end
end
local member = resolve_member_path(root_type, member_path, all_types)
local member, failed_at = resolve_member_path(root_type, member_path, all_types, external_types)
if member then
local detail = string.format("**Member**: `%s`\nType: `%s`",
member.name, member.type or "unknown")
@@ -468,7 +478,20 @@ function handlers.textDocument_hover(params)
["end"] = { line = params.position.line, character = member_word_end - 1 },
},
}
elseif failed_at then
-- Type resolution failed - show what we know
local detail = string.format("**Unresolved Member**\nVariable: `#%s`\nType: `%s`\nPath: `%s`\n\n*Type not found: `%s`*",
root_var, root_type, member_path, failed_at)
return {
contents = { kind = "markdown", value = detail },
}
end
else
-- Variable not found
local detail = string.format("**Unknown Variable**: `#%s`\n\nVariable not declared in scope.", root_var)
return {
contents = { kind = "markdown", value = detail },
}
end
end