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:
+159
@@ -313,6 +313,165 @@ function handlers.textDocument_hover(params)
|
||||
local line = lines[line_idx]
|
||||
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
|
||||
while word_start > 1 and line:sub(word_start - 1, word_start - 1):match("[%w_]") do
|
||||
word_start = word_start - 1
|
||||
|
||||
@@ -212,9 +212,11 @@ local function parse_scl_type_file(content)
|
||||
local field_name = line:match("^%s*(%w+)%s*:")
|
||||
if field_name and field_name ~= "END_STRUCT" then
|
||||
local field_type = line:match(":%s*([%w_]+)")
|
||||
local field_comment = line:match("//%s*(.+)$") or line:match("%(%*(.-)%*%)")
|
||||
table.insert(types[current_type_name].fields, {
|
||||
name = field_name,
|
||||
type = field_type or "unknown",
|
||||
comment = field_comment,
|
||||
})
|
||||
end
|
||||
end
|
||||
@@ -254,10 +256,12 @@ local function parse_db_file(content)
|
||||
local field_name = line:match("^%s*(%w+)%s*:")
|
||||
if field_name then
|
||||
local field_type = line:match(":%s*([%w_]+)")
|
||||
local field_comment = line:match("//%s*(.+)$") or line:match("%(%*(.-)%*%)")
|
||||
if field_type then
|
||||
table.insert(types[current_type_name].fields, {
|
||||
name = field_name,
|
||||
type = field_type,
|
||||
comment = field_comment,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user