From d56ea306af833162cefa4555bdfc4a0b18dda89f Mon Sep 17 00:00:00 2001 From: Lazar Bulovan Date: Sat, 21 Feb 2026 00:32:55 +0100 Subject: [PATCH] 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 --- src/main.lua | 139 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 81 insertions(+), 58 deletions(-) diff --git a/src/main.lua b/src/main.lua index 3436ca8..7232070 100644 --- a/src/main.lua +++ b/src/main.lua @@ -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