Compare commits
7
Commits
v0.1.6
...
dae99fb33e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dae99fb33e | ||
|
|
8d5bf9f99f | ||
|
|
2184007cf6 | ||
|
|
3abb0f5d9a | ||
|
|
a33d15d238 | ||
|
|
eb62107066 | ||
|
|
00cf1b0074 |
@@ -24,7 +24,7 @@ lua test/test_formatter.lua # Run formatter tests
|
||||
lua test/test_plc_json.lua # Run plc_json tests
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
@@ -152,7 +152,7 @@ The LSP validates data types and provides warnings for unknown types:
|
||||
|
||||
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:
|
||||
|
||||
@@ -109,6 +109,9 @@ function M.validate_diagnostics(content, workspace_types, root_dir)
|
||||
EXTERNALWRITABLE = true, -- all-caps VCI export variant
|
||||
EXTERNALVISIBLE = true, -- all-caps VCI export variant
|
||||
EXTERNALACCESSIBLE = true,-- all-caps VCI export variant
|
||||
InstructionName = true,
|
||||
LibVersion = true,
|
||||
LibName = true,
|
||||
S7_SetPoint = true,
|
||||
S7_Optimized_Access = true,
|
||||
}
|
||||
|
||||
+164
-21
@@ -717,6 +717,72 @@ function handlers.textDocument_hover(params)
|
||||
return nil
|
||||
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():sub(1, #prefix_filter) == prefix_filter:upper() 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():sub(1, #prefix_filter) == prefix_filter:upper() 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
|
||||
--- Provides completion items based on context (VAR section, member access, etc.)
|
||||
--- @param params table Contains textDocument with uri and position
|
||||
@@ -738,39 +804,47 @@ function handlers.textDocument_completion(params)
|
||||
if line_idx <= #lines then
|
||||
local line = lines[line_idx]
|
||||
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 ""
|
||||
|
||||
-- Get word prefix being typed (for filtering)
|
||||
local prefix_start = col - 1
|
||||
-- Start from col (not col-1) so delimiters like ( stop the scan
|
||||
local prefix_start = col
|
||||
while prefix_start > 0 and line:sub(prefix_start, prefix_start):match("[%w_]") do
|
||||
prefix_start = prefix_start - 1
|
||||
end
|
||||
local word_prefix = line:sub(prefix_start + 1, col - 1)
|
||||
local word_prefix = col > prefix_start and line:sub(prefix_start + 1, col) or ""
|
||||
|
||||
-- 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
|
||||
for i = line_idx - 1, 1, -1 do
|
||||
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
|
||||
end
|
||||
if prev_line:match("^VAR%s*$") or prev_line:match("^VAR$") then
|
||||
if prev_line:match("^VAR") then
|
||||
in_var_section = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if trigger == "#" then
|
||||
if trigger == "#" or has_hash_prefix 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
|
||||
table.insert(items, {
|
||||
label = "#" .. name,
|
||||
kind = 13,
|
||||
detail = "Local variable (" .. (info.type or "unknown") .. ")",
|
||||
insertText = "#" .. name,
|
||||
insertTextFormat = 1,
|
||||
})
|
||||
if prefix == "" or name:upper():sub(1, #prefix) == prefix:upper() then
|
||||
table.insert(items, {
|
||||
label = "#" .. name,
|
||||
filterText = "#" .. name,
|
||||
kind = 13,
|
||||
detail = "Variable (" .. (info.type or "VAR") .. "): " .. (info.data_type or "unknown"),
|
||||
insertText = "#" .. name,
|
||||
insertTextFormat = 1,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif trigger == "." then
|
||||
@@ -837,18 +911,45 @@ function handlers.textDocument_completion(params)
|
||||
end
|
||||
end
|
||||
elseif trigger == "(" then
|
||||
if doc.functions then
|
||||
for _, func in ipairs(doc.functions) do
|
||||
-- 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, "")
|
||||
end
|
||||
|
||||
-- Also add variables and builtins so completion popup stays rich
|
||||
-- even if auto-close ( or client-side filtering interferes
|
||||
if doc.variables then
|
||||
for name, info in pairs(doc.variables) do
|
||||
table.insert(items, {
|
||||
label = func.name,
|
||||
kind = 3,
|
||||
detail = func.kind:upper(),
|
||||
insertText = func.name .. "()",
|
||||
insertTextFormat = 2,
|
||||
label = "#" .. name,
|
||||
filterText = "#" .. name,
|
||||
kind = 13,
|
||||
detail = "Variable (" .. (info.type or "VAR") .. "): " .. (info.data_type or "unknown"),
|
||||
insertText = "#" .. name,
|
||||
insertTextFormat = 1,
|
||||
})
|
||||
end
|
||||
end
|
||||
for type_name, _ in pairs(builtin.DATA_TYPES) do
|
||||
table.insert(items, { label = type_name, kind = 7, detail = "Data type", insertText = type_name, insertTextFormat = 1 })
|
||||
end
|
||||
for fb_name, _ in pairs(builtin.FUNCTION_BLOCKS) do
|
||||
table.insert(items, { label = fb_name, kind = 6, detail = "Function block", insertText = fb_name, insertTextFormat = 1 })
|
||||
end
|
||||
for func_name, _ in pairs(builtin.FUNCTIONS) do
|
||||
table.insert(items, { label = func_name, kind = 3, detail = "Function", insertText = func_name, insertTextFormat = 1 })
|
||||
end
|
||||
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
|
||||
local is_var_decl = line_before:match(":%s*$")
|
||||
|
||||
@@ -881,6 +982,27 @@ function handlers.textDocument_completion(params)
|
||||
add_item(type_name, 7, type_info.kind or "Type")
|
||||
end
|
||||
end
|
||||
|
||||
-- Add declared variables (label without # so client-side matching works)
|
||||
if doc.variables then
|
||||
for name, info in pairs(doc.variables) do
|
||||
if word_prefix == "" or name:upper():sub(1, #word_prefix) == word_prefix:upper() then
|
||||
table.insert(items, {
|
||||
label = name,
|
||||
filterText = 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
|
||||
-- General context: show all
|
||||
for type_name, _ in pairs(builtin.DATA_TYPES) do
|
||||
@@ -894,6 +1016,27 @@ function handlers.textDocument_completion(params)
|
||||
for func_name, _ in pairs(builtin.FUNCTIONS) do
|
||||
add_item(func_name, 3, "Function")
|
||||
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():sub(1, #word_prefix) == word_prefix:upper() then
|
||||
table.insert(items, {
|
||||
label = name,
|
||||
filterText = 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
|
||||
|
||||
Reference in New Issue
Block a user