fix: show function/block parameters in autocomplete after typing (

This commit is contained in:
2026-07-22 13:47:11 +02:00
parent 3ba1ccbb1b
commit 00cf1b0074
2 changed files with 140 additions and 15 deletions
+2 -2
View File
@@ -24,7 +24,7 @@ lua test/test_formatter.lua # Run formatter tests
lua test/test_plc_json.lua # Run plc_json tests lua test/test_plc_json.lua # Run plc_json tests
# Reference Project # Reference Project
# Tests use files from: ~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project/ # Tests use files from: ~/Documents/siemens/scl_lang_support_lazyvim_ref_project/
# Override with: SCL_REF_PROJECT=/path/to/ref lua tests/run_tests.lua # Override with: SCL_REF_PROJECT=/path/to/ref lua tests/run_tests.lua
``` ```
@@ -152,7 +152,7 @@ The LSP validates data types and provides warnings for unknown types:
Use the reference project for testing: Use the reference project for testing:
``` ```
~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project/ ~/Documenta/siemens/scl_lang_support_lazyvim_ref_project/
``` ```
Contains `.scl`, `.db`, `.udt`, and `.xml` files for comprehensive testing of: Contains `.scl`, `.db`, `.udt`, and `.xml` files for comprehensive testing of:
+138 -13
View File
@@ -717,6 +717,72 @@ function handlers.textDocument_hover(params)
return nil return nil
end end
--- Detect if cursor is inside a function call's parentheses
--- Scans backwards from cursor to find an unmatched ( and returns the name before it
--- @param line string The current line content
--- @param col number Cursor column (0-indexed LSP position)
--- @return string|nil Function/block name, or nil if not in a call context
local function get_call_context(line, col)
if col <= 1 then return nil end
local before_cursor = line:sub(1, col)
local paren_pos = nil
for i = #before_cursor, 1, -1 do
local c = before_cursor:sub(i, i)
if c == ")" then break end
if c == "(" then paren_pos = i; break end
end
if not paren_pos then return nil end
local before_paren = before_cursor:sub(1, paren_pos - 1)
return before_paren:match("([%w_]+)%s*$")
end
--- Resolve a function/variable call name to its parameter info
--- Checks variables (for data_type resolution), built-in parameters, and workspace types
--- @param call_name string The name to resolve (e.g., "instTestTimer" or "TON")
--- @param variables table|nil Document variables table
--- @param types table|nil Document types table
--- @return table|nil Parameter definition with inputs and outputs arrays, or nil
local function resolve_call_params(call_name, variables, types)
if not call_name then return nil end
local clean_name = call_name:gsub("^#", "")
local data_type = variables and variables[clean_name] and variables[clean_name].data_type
local resolved_name = data_type or clean_name
local builtin_params = builtin.get_parameters(resolved_name)
if builtin_params then return builtin_params end
local type_info = types and types[resolved_name]
if type_info and type_info.fields then
return { inputs = type_info.fields, outputs = {} }
end
return nil
end
local function add_param_items(items, params, prefix_filter)
for _, inp in ipairs(params.inputs or {}) do
local label = inp.name .. " := "
if prefix_filter == "" or inp.name:upper():find("^" .. prefix_filter:upper(), 1, true) then
table.insert(items, {
label = label,
kind = 13,
detail = "Parameter (" .. (inp.type or "ANY") .. ")",
insertText = inp.name .. " := ",
insertTextFormat = 1,
})
end
end
for _, out in ipairs(params.outputs or {}) do
local label = out.name .. " => "
if prefix_filter == "" or out.name:upper():find("^" .. prefix_filter:upper(), 1, true) then
table.insert(items, {
label = label,
kind = 13,
detail = "Output (" .. (out.type or "ANY") .. ")",
insertText = out.name .. " => ",
insertTextFormat = 1,
})
end
end
end
--- Handle textDocument/completion request --- Handle textDocument/completion request
--- Provides completion items based on context (VAR section, member access, etc.) --- Provides completion items based on context (VAR section, member access, etc.)
--- @param params table Contains textDocument with uri and position --- @param params table Contains textDocument with uri and position
@@ -738,7 +804,7 @@ function handlers.textDocument_completion(params)
if line_idx <= #lines then if line_idx <= #lines then
local line = lines[line_idx] local line = lines[line_idx]
local col = params.position.character local col = params.position.character
local trigger = col > 0 and line:sub(col, col) or "" local trigger = (params.context and params.context.triggerCharacter) or (col > 0 and line:sub(col, col) or "")
local line_before = col > 1 and line:sub(1, col - 1) or "" local line_before = col > 1 and line:sub(1, col - 1) or ""
-- Get word prefix being typed (for filtering) -- Get word prefix being typed (for filtering)
@@ -748,29 +814,35 @@ function handlers.textDocument_completion(params)
end end
local word_prefix = line:sub(prefix_start + 1, col - 1) local word_prefix = line:sub(prefix_start + 1, col - 1)
-- Check if we're in VAR section -- Check if typing after # prefix (e.g., #myVar)
local has_hash_prefix = line_before:match("#[%w_]*$")
-- Check if we're in VAR section (detects all VAR variants)
local in_var_section = false local in_var_section = false
for i = line_idx - 1, 1, -1 do for i = line_idx - 1, 1, -1 do
local prev_line = lines[i]:gsub("^%s+", ""):gsub("%s+$", "") local prev_line = lines[i]:gsub("^%s+", ""):gsub("%s+$", "")
if prev_line:match("^END_VAR") or prev_line:match("^VAR_TEMP") or prev_line:match("^VAR_IN_OUT") or prev_line:match("^VAR_OUTPUT") or prev_line:match("^VAR_INPUT") then if prev_line:match("^END_VAR") then
break break
end end
if prev_line:match("^VAR%s*$") or prev_line:match("^VAR$") then if prev_line:match("^VAR") then
in_var_section = true in_var_section = true
break break
end end
end end
if trigger == "#" then if trigger == "#" or has_hash_prefix then
if doc.variables then if doc.variables then
local prefix = has_hash_prefix and line_before:match("#([%w_]*)$") or word_prefix
for name, info in pairs(doc.variables) do for name, info in pairs(doc.variables) do
table.insert(items, { if prefix == "" or name:upper():find("^" .. prefix:upper(), 1, true) then
label = "#" .. name, table.insert(items, {
kind = 13, label = "#" .. name,
detail = "Local variable (" .. (info.type or "unknown") .. ")", kind = 13,
insertText = "#" .. name, detail = "Variable (" .. (info.type or "VAR") .. "): " .. (info.data_type or "unknown"),
insertTextFormat = 1, insertText = "#" .. name,
}) insertTextFormat = 1,
})
end
end end
end end
elseif trigger == "." then elseif trigger == "." then
@@ -837,7 +909,16 @@ function handlers.textDocument_completion(params)
end end
end end
elseif trigger == "(" then elseif trigger == "(" then
if doc.functions then -- Try get_call_context first (handles case where ( is already in the line)
local call_name = get_call_context(line, col)
-- Fallback: ( may not be in the line yet (client sends completion before didChange)
if not call_name and col > 0 then
call_name = line:sub(1, col):match("([%w_]+)%s*$")
end
local params = call_name and resolve_call_params(call_name, doc.variables, doc.types)
if params then
add_param_items(items, params, "")
elseif doc.functions then
for _, func in ipairs(doc.functions) do for _, func in ipairs(doc.functions) do
table.insert(items, { table.insert(items, {
label = func.name, label = func.name,
@@ -849,6 +930,10 @@ function handlers.textDocument_completion(params)
end end
end end
else else
-- Check if we're inside a function call (e.g., after instTestTimer()
local call_name = get_call_context(line, col)
local call_params = call_name and resolve_call_params(call_name, doc.variables, doc.types)
-- Check context before cursor -- Check context before cursor
local is_var_decl = line_before:match(":%s*$") local is_var_decl = line_before:match(":%s*$")
@@ -881,6 +966,26 @@ function handlers.textDocument_completion(params)
add_item(type_name, 7, type_info.kind or "Type") add_item(type_name, 7, type_info.kind or "Type")
end end
end end
-- Add declared variables (without # prefix in label, insert with #)
if doc.variables then
for name, info in pairs(doc.variables) do
if word_prefix == "" or name:upper():find("^" .. word_prefix:upper(), 1, true) then
table.insert(items, {
label = "#" .. name,
kind = 13,
detail = "Variable (" .. (info.type or "VAR") .. "): " .. (info.data_type or "unknown"),
insertText = "#" .. name,
insertTextFormat = 1,
})
end
end
end
-- Add function/block call parameters if inside (...) context
if call_params then
add_param_items(items, call_params, word_prefix)
end
else else
-- General context: show all -- General context: show all
for type_name, _ in pairs(builtin.DATA_TYPES) do for type_name, _ in pairs(builtin.DATA_TYPES) do
@@ -894,6 +999,26 @@ function handlers.textDocument_completion(params)
for func_name, _ in pairs(builtin.FUNCTIONS) do for func_name, _ in pairs(builtin.FUNCTIONS) do
add_item(func_name, 3, "Function") add_item(func_name, 3, "Function")
end end
-- Add declared variables in general context too
if doc.variables then
for name, info in pairs(doc.variables) do
if word_prefix == "" or name:upper():find("^" .. word_prefix:upper(), 1, true) then
table.insert(items, {
label = "#" .. name,
kind = 13,
detail = "Variable (" .. (info.type or "VAR") .. "): " .. (info.data_type or "unknown"),
insertText = "#" .. name,
insertTextFormat = 1,
})
end
end
end
-- Add function/block call parameters if inside (...) context
if call_params then
add_param_items(items, call_params, word_prefix)
end
end end
end end
end end