feat: add hover support for member access with comments

- Add member access chain resolution in hover handler
- Parse #var.member.submember notation and resolve type chain
- Show member comments in hover tooltip
- Extract comments from UDT and DB file fields
- Combine all type sources (doc.types, external_types, builtin)
This commit is contained in:
2026-02-21 00:13:52 +01:00
parent 5282385e79
commit 1606685a20
2 changed files with 163 additions and 0 deletions
+159
View File
@@ -313,6 +313,165 @@ function handlers.textDocument_hover(params)
local line = lines[line_idx] local line = lines[line_idx]
local col = params.position.character + 1 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)
if not root_type or not member_path or member_path == "" then
return nil
end
local parts = {}
for part in member_path:gmatch("[^.]+") do
table.insert(parts, part)
end
if #parts == 0 then
return 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]
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
end
if not found then
return nil
end
end
end
return current_member
end
-- Check if we're in a member access chain (e.g., #var.member1.member2)
local function find_member_access_chain()
local chain_start = col
while chain_start > 1 do
local ch = line:sub(chain_start - 1, chain_start - 1)
if ch:match("[%w_.#]") then
chain_start = chain_start - 1
else
break
end
end
local chain_end = col
while chain_end <= #line do
local ch = line:sub(chain_end, chain_end)
if ch:match("[%w_.]") then
chain_end = chain_end + 1
else
break
end
end
local chain = line:sub(chain_start, chain_end - 1)
-- 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_]+)%.")
end
if root_var then
local member_path = chain:match("%.(.+)$")
return root_var, member_path, chain_start, chain_end
end
return nil, nil, chain_start, chain_end
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 all_types = {}
-- Combine all type sources
if doc.types then
for k, v in pairs(doc.types) do
all_types[k] = v
end
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)
if member then
local detail = string.format("**Member**: `%s`\nType: `%s`",
member.name, member.type or "unknown")
if member.comment and member.comment ~= "" then
detail = detail .. "\n\n" .. member.comment
end
detail = detail .. string.format("\n\n*From: `#%s.%s`*", root_var, member_path)
-- Find the actual word range for highlighting
local member_word_start = col
while member_word_start > 1 and line:sub(member_word_start - 1, member_word_start - 1):match("[%w_]") do
member_word_start = member_word_start - 1
end
local member_word_end = col
while member_word_end <= #line and line:sub(member_word_end, member_word_end):match("[%w_]") do
member_word_end = member_word_end + 1
end
return {
contents = { kind = "markdown", value = detail },
range = {
start = { line = params.position.line, character = member_word_start - 1 },
["end"] = { line = params.position.line, character = member_word_end - 1 },
},
}
end
end
end
local word_start = col local word_start = col
while word_start > 1 and line:sub(word_start - 1, word_start - 1):match("[%w_]") do while word_start > 1 and line:sub(word_start - 1, word_start - 1):match("[%w_]") do
word_start = word_start - 1 word_start = word_start - 1
+4
View File
@@ -212,9 +212,11 @@ local function parse_scl_type_file(content)
local field_name = line:match("^%s*(%w+)%s*:") local field_name = line:match("^%s*(%w+)%s*:")
if field_name and field_name ~= "END_STRUCT" then if field_name and field_name ~= "END_STRUCT" then
local field_type = line:match(":%s*([%w_]+)") local field_type = line:match(":%s*([%w_]+)")
local field_comment = line:match("//%s*(.+)$") or line:match("%(%*(.-)%*%)")
table.insert(types[current_type_name].fields, { table.insert(types[current_type_name].fields, {
name = field_name, name = field_name,
type = field_type or "unknown", type = field_type or "unknown",
comment = field_comment,
}) })
end end
end end
@@ -254,10 +256,12 @@ local function parse_db_file(content)
local field_name = line:match("^%s*(%w+)%s*:") local field_name = line:match("^%s*(%w+)%s*:")
if field_name then if field_name then
local field_type = line:match(":%s*([%w_]+)") local field_type = line:match(":%s*([%w_]+)")
local field_comment = line:match("//%s*(.+)$") or line:match("%(%*(.-)%*%)")
if field_type then if field_type then
table.insert(types[current_type_name].fields, { table.insert(types[current_type_name].fields, {
name = field_name, name = field_name,
type = field_type, type = field_type,
comment = field_comment,
}) })
end end
end end