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 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user