Compare commits
10
Commits
dae99fb33e
...
b4293b4f30
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4293b4f30 | ||
|
|
6fbce2af6b | ||
|
|
909354e71a | ||
|
|
d24e83a1a7 | ||
|
|
ebdc96316a | ||
|
|
3ca63d278b | ||
|
|
8b400054c6 | ||
|
|
cce4059b7e | ||
|
|
5205819201 | ||
|
|
73735192bd |
+31
-1
@@ -138,12 +138,21 @@ function M.validate_diagnostics(content, workspace_types, root_dir)
|
||||
return prefix == "T" or prefix == "D" or prefix == "S"
|
||||
end
|
||||
|
||||
-- Check if hash is part of a base-specific number literal like 16#FFFF, 2#1010
|
||||
local function is_number_literal_at(line, match_start)
|
||||
if not line or not match_start then return false end
|
||||
if match_start <= 1 then return false end
|
||||
local before = line:sub(match_start - 1, match_start - 1)
|
||||
return before:match("%d") ~= nil
|
||||
end
|
||||
|
||||
if not in_var_section then
|
||||
if not (trimmed:match("^%s*//") or trimmed:match("^%s*%(%*")) then
|
||||
local hash_vars = {}
|
||||
for var in line:gmatch("#[%w_]+") do
|
||||
local var_name = var:sub(2)
|
||||
local match_start = line:find(var, 1, true)
|
||||
if not is_iec_literal_at(line, match_start) then
|
||||
if not is_iec_literal_at(line, match_start) and not is_number_literal_at(line, match_start) then
|
||||
hash_vars[var_name] = true
|
||||
end
|
||||
end
|
||||
@@ -163,6 +172,7 @@ function M.validate_diagnostics(content, workspace_types, root_dir)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if in_var_section and (trimmed:match("^[%w_]+%s*%b{}%s*:") or trimmed:match("^[%w_]+%s*:")) then
|
||||
local var_name = trimmed:match("^([%w_]+)%s*%b{}%s*:") or trimmed:match("^([%w_]+)%s*:")
|
||||
@@ -246,6 +256,26 @@ function M.validate_diagnostics(content, workspace_types, root_dir)
|
||||
if trimmed:match("^%s*//") or trimmed:match("^%s*%(%*") then
|
||||
-- Skip comments
|
||||
else
|
||||
-- Check for unassigned parameters in FB/FC calls
|
||||
for unassigned in line:gmatch("([%w_]+)%s*:=%s*[,%)]") do
|
||||
table.insert(diagnostics, {
|
||||
range = range_to_lsp(line_num, line:find(unassigned, 1, true) - 1, line_num, line:find(unassigned, 1, true) + #unassigned),
|
||||
severity = DiagnosticSeverity.Warning,
|
||||
message = string.format("Unassigned input parameter: %s :=", unassigned),
|
||||
code = "SCL006",
|
||||
source = "tia_lsp",
|
||||
})
|
||||
end
|
||||
for unassigned in line:gmatch("([%w_]+)%s*=>%s*[,%)]") do
|
||||
table.insert(diagnostics, {
|
||||
range = range_to_lsp(line_num, line:find(unassigned, 1, true) - 1, line_num, line:find(unassigned, 1, true) + #unassigned),
|
||||
severity = DiagnosticSeverity.Warning,
|
||||
message = string.format("Unassigned output parameter: %s =>", unassigned),
|
||||
code = "SCL006",
|
||||
source = "tia_lsp",
|
||||
})
|
||||
end
|
||||
|
||||
for assignment in line:gmatch("#[%w_]+%s*:=") do
|
||||
local var_name = assignment:match("#([%w_]+)")
|
||||
if var_name and in_var_section then
|
||||
|
||||
+59
-28
@@ -43,6 +43,7 @@ local BLOCKS = {
|
||||
["FUNCTION"] = "END_FUNCTION",
|
||||
["FUNCTION_BLOCK"] = "END_FUNCTION_BLOCK",
|
||||
["ORGANIZATION_BLOCK"] = "END_ORGANIZATION_BLOCK",
|
||||
["DATA_BLOCK"] = "END_DATA_BLOCK",
|
||||
["TYPE"] = "END_TYPE",
|
||||
["STRUCT"] = "END_STRUCT",
|
||||
["VAR"] = "END_VAR",
|
||||
@@ -72,7 +73,6 @@ local NEEDS_SEMICOLON = {
|
||||
["END_WHILE"] = true,
|
||||
["END_REPEAT"] = true,
|
||||
["END_CASE"] = true,
|
||||
["END_REGION"] = true,
|
||||
}
|
||||
|
||||
local VAR_KEYWORDS = {
|
||||
@@ -92,6 +92,7 @@ local DECLARATIONS = {
|
||||
["FUNCTION"] = true,
|
||||
["FUNCTION_BLOCK"] = true,
|
||||
["ORGANIZATION_BLOCK"] = true,
|
||||
["DATA_BLOCK"] = true,
|
||||
["TYPE"] = true,
|
||||
["STRUCT"] = true,
|
||||
}
|
||||
@@ -111,9 +112,6 @@ local CONTINUATION_OPERATORS = {
|
||||
--- @return table Array of TextEdit objects with range and newText
|
||||
function M.format_document(content, options)
|
||||
options = options or {}
|
||||
local use_spaces = options.insertSpaces or false
|
||||
local indent_size = options.tabSize or 1
|
||||
local indent_char = use_spaces and string.rep(" ", indent_size) or "\t"
|
||||
|
||||
local collapse_attributes = options.collapseAttributes
|
||||
if collapse_attributes == nil then
|
||||
@@ -127,6 +125,11 @@ function M.format_document(content, options)
|
||||
end
|
||||
end
|
||||
|
||||
local bom = string.char(239, 187, 191)
|
||||
if content:sub(1, 3) == bom then
|
||||
content = content:sub(4)
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for line in content:gmatch("([^\n]*)\n") do
|
||||
table.insert(lines, line)
|
||||
@@ -141,15 +144,29 @@ function M.format_document(content, options)
|
||||
|
||||
local stack = {}
|
||||
local formatted_lines = {}
|
||||
local in_code_body = false
|
||||
local in_var_block = false
|
||||
|
||||
local function get_indent()
|
||||
if in_code_body then
|
||||
local level = 0
|
||||
for _, block in ipairs(stack) do
|
||||
if not block.no_indent then
|
||||
level = level + 1
|
||||
end
|
||||
end
|
||||
return string.rep(indent_char, level)
|
||||
return "\t" .. string.rep(" ", level)
|
||||
else
|
||||
if in_var_block then
|
||||
return " "
|
||||
else
|
||||
return ""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function get_code_indent(level)
|
||||
return "\t" .. string.rep(" ", level)
|
||||
end
|
||||
|
||||
local function is_assignment_continuation(trimmed)
|
||||
@@ -261,6 +278,15 @@ function M.format_document(content, options)
|
||||
table.insert(params, last_param)
|
||||
end
|
||||
|
||||
-- Remove unassigned parameters (e.g. "IN :=", "Q =>", "ET =>")
|
||||
local filtered = {}
|
||||
for _, p in ipairs(params) do
|
||||
if not p:match("^[%w_]+%s*:=%s*$") and not p:match("^[%w_]+%s*=>%s*$") then
|
||||
table.insert(filtered, p)
|
||||
end
|
||||
end
|
||||
params = filtered
|
||||
|
||||
if #params == 0 then
|
||||
return { base_indent .. fb_name .. "();" }
|
||||
end
|
||||
@@ -276,7 +302,7 @@ function M.format_document(content, options)
|
||||
end
|
||||
|
||||
local function is_case_label(trimmed)
|
||||
return trimmed:match("^[%w_#]+%s*:%s*$") ~= nil
|
||||
return trimmed:match("^\"[^\"]+\"%s*:%s*$") ~= nil or trimmed:match("^[%w_#]+%s*:%s*$") ~= nil
|
||||
end
|
||||
|
||||
local function extract_variable_name(line)
|
||||
@@ -489,7 +515,7 @@ function M.format_document(content, options)
|
||||
if DECLARATIONS[kw] then
|
||||
stack = {}
|
||||
table.insert(formatted_lines, trimmed)
|
||||
table.insert(stack, { type = kw, indent = 0, is_declaration = true })
|
||||
table.insert(stack, { type = kw, indent = 0, is_declaration = true, no_indent = true })
|
||||
goto continue
|
||||
end
|
||||
|
||||
@@ -507,14 +533,14 @@ function M.format_document(content, options)
|
||||
while #stack > 0 and VAR_KEYWORDS[stack[#stack].type] do
|
||||
table.remove(stack)
|
||||
end
|
||||
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||
in_var_block = false
|
||||
in_code_body = true
|
||||
table.insert(formatted_lines, trimmed)
|
||||
goto continue
|
||||
end
|
||||
|
||||
if in_code_body then
|
||||
if VAR_KEYWORDS[kw] then
|
||||
while #stack > 0 and not stack[#stack].is_declaration do
|
||||
table.remove(stack)
|
||||
end
|
||||
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||
table.insert(stack, { type = kw, indent = #stack, is_var = true })
|
||||
goto continue
|
||||
@@ -527,6 +553,19 @@ function M.format_document(content, options)
|
||||
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||
goto continue
|
||||
end
|
||||
else
|
||||
if VAR_KEYWORDS[kw] then
|
||||
in_var_block = true
|
||||
table.insert(formatted_lines, trimmed)
|
||||
goto continue
|
||||
end
|
||||
|
||||
if kw == "END_VAR" then
|
||||
in_var_block = false
|
||||
table.insert(formatted_lines, trimmed)
|
||||
goto continue
|
||||
end
|
||||
end
|
||||
|
||||
if kw and BLOCKS[kw] == nil then
|
||||
for starter, ender in pairs(BLOCKS) do
|
||||
@@ -556,7 +595,7 @@ function M.format_document(content, options)
|
||||
break
|
||||
end
|
||||
end
|
||||
then_indent = if_block and string.rep(indent_char, if_block.indent) or get_indent()
|
||||
then_indent = if_block and get_code_indent(if_block.indent) or get_indent()
|
||||
end
|
||||
table.insert(formatted_lines, then_indent .. trimmed)
|
||||
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
||||
@@ -573,7 +612,7 @@ function M.format_document(content, options)
|
||||
end
|
||||
end
|
||||
if if_block then
|
||||
formatted = string.rep(indent_char, if_block.indent) .. trimmed
|
||||
formatted = get_code_indent(if_block.indent) .. trimmed
|
||||
else
|
||||
formatted = get_indent() .. trimmed
|
||||
end
|
||||
@@ -582,7 +621,7 @@ function M.format_document(content, options)
|
||||
-- Check if this is a multi-line condition (doesn't end with THEN on same line)
|
||||
if not trimmed:match("THEN%s*$") then
|
||||
in_condition = true
|
||||
condition_indent = string.rep(indent_char, (if_block and if_block.indent or 0) + 1)
|
||||
condition_indent = get_code_indent((if_block and if_block.indent or 0) + 1)
|
||||
end
|
||||
else
|
||||
formatted = get_indent() .. trimmed
|
||||
@@ -597,9 +636,10 @@ function M.format_document(content, options)
|
||||
while #stack > case_idx do
|
||||
table.remove(stack)
|
||||
end
|
||||
local label_indent = string.rep(indent_char, case_block.indent + 1)
|
||||
local label_indent = get_code_indent(case_block.indent + 1)
|
||||
table.insert(formatted_lines, label_indent .. trimmed)
|
||||
table.insert(stack, { type = "CASE_LABEL", indent = case_block.indent + 1, no_indent = true })
|
||||
table.insert(stack, { type = "CASE_CONTENT", indent = case_block.indent + 2 })
|
||||
else
|
||||
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||
end
|
||||
@@ -613,7 +653,7 @@ function M.format_document(content, options)
|
||||
current_level = current_level + 1
|
||||
end
|
||||
end
|
||||
local formatted = string.rep(indent_char, current_level) .. trimmed
|
||||
local formatted = get_code_indent(current_level) .. trimmed
|
||||
table.insert(formatted_lines, formatted)
|
||||
table.insert(stack, { type = kw, indent = current_level })
|
||||
if kw == "IF" or kw == "ELSIF" then
|
||||
@@ -621,33 +661,24 @@ function M.format_document(content, options)
|
||||
if not trimmed:match("THEN%s*$") then
|
||||
in_condition = true
|
||||
-- Continuation lines should be indented one level deeper than IF/ELSIF
|
||||
condition_indent = string.rep(indent_char, current_level + 1)
|
||||
condition_indent = get_code_indent(current_level + 1)
|
||||
end
|
||||
end
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Regular line - check for assignment start
|
||||
-- But not if its inside an attribute block, exiting one, or a single-line attribute block
|
||||
if not in_attr_block and not exiting_attr_block and trimmed:match(":=") and not trimmed:match("^{.*}$") then
|
||||
local base_indent = get_indent()
|
||||
local assign_pos = trimmed:find(":=")
|
||||
if assign_pos then
|
||||
local var_part = trimmed:sub(1, assign_pos - 1)
|
||||
local var_trimmed = var_part:match("^(.-)%s*$")
|
||||
assignment_continuation_indent = base_indent .. string.rep(" ", #var_trimmed + 4)
|
||||
|
||||
if in_code_body and not in_attr_block and not exiting_attr_block and trimmed:match(":=") and not trimmed:match("^{.*}$") then
|
||||
if trimmed:match(";%s*$") then
|
||||
in_assignment = false
|
||||
else
|
||||
in_assignment = true
|
||||
end
|
||||
assignment_continuation_indent = get_indent()
|
||||
end
|
||||
else
|
||||
in_assignment = false
|
||||
end
|
||||
|
||||
-- Also reset assignment state when in attribute block
|
||||
if in_attr_block then
|
||||
in_assignment = false
|
||||
end
|
||||
|
||||
+25
-9
@@ -71,6 +71,9 @@ local capabilities = {
|
||||
workspaceSymbolProvider = true,
|
||||
documentFormattingProvider = true,
|
||||
documentRangeFormattingProvider = false,
|
||||
executeCommandProvider = {
|
||||
commands = { "SCLRescanWorkspaceTypes" },
|
||||
},
|
||||
}
|
||||
|
||||
local function uri_to_path(uri)
|
||||
@@ -831,11 +834,10 @@ function handlers.textDocument_completion(params)
|
||||
end
|
||||
end
|
||||
|
||||
if trigger == "#" or has_hash_prefix then
|
||||
if trigger == "#" 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
|
||||
if prefix == "" or name:upper():sub(1, #prefix) == prefix:upper() then
|
||||
if word_prefix == "" or name:upper():sub(1, #word_prefix) == word_prefix:upper() then
|
||||
table.insert(items, {
|
||||
label = "#" .. name,
|
||||
filterText = "#" .. name,
|
||||
@@ -848,11 +850,16 @@ function handlers.textDocument_completion(params)
|
||||
end
|
||||
end
|
||||
elseif trigger == "." then
|
||||
local prefix_start = col - 1
|
||||
local scan_end = col
|
||||
local c = col > 0 and line:sub(col, col) or ""
|
||||
if c == "" or c == "." then
|
||||
scan_end = col - 1
|
||||
end
|
||||
local prefix_start = scan_end
|
||||
while prefix_start > 0 and line:sub(prefix_start, prefix_start):match("[%w_]") do
|
||||
prefix_start = prefix_start - 1
|
||||
end
|
||||
local prefix = line:sub(prefix_start + 1, col - 1)
|
||||
local prefix = scan_end > prefix_start and line:sub(prefix_start + 1, scan_end) or ""
|
||||
|
||||
if prefix ~= "" then
|
||||
local var_name = prefix:gsub("^#", "")
|
||||
@@ -1798,16 +1805,25 @@ function handlers.textDocument_formatting(params)
|
||||
return nil
|
||||
end
|
||||
|
||||
local options = params.options or {}
|
||||
local format_options = {
|
||||
insertSpaces = options.insertSpaces,
|
||||
tabSize = options.tabSize,
|
||||
indentSize = options.tabSize or 4,
|
||||
insertSpaces = false,
|
||||
tabSize = 1,
|
||||
indentSize = 1,
|
||||
}
|
||||
|
||||
return formatter.format_document(doc.content, format_options)
|
||||
end
|
||||
|
||||
function handlers.workspace_executeCommand(params)
|
||||
if params.command ~= "SCLRescanWorkspaceTypes" then
|
||||
return nil
|
||||
end
|
||||
|
||||
plc_json.clear_cache()
|
||||
|
||||
return {}
|
||||
end
|
||||
|
||||
local function handle_message(message)
|
||||
if message.method == "$/cancelRequest" then
|
||||
return nil
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ local function parse_scl_type_file(content)
|
||||
fb_inouts = {}
|
||||
current_fb_section = nil
|
||||
elseif in_fb and trimmed:match("^END_FUNCTION_BLOCK") then
|
||||
if fb_name and (#fb_inputs > 0 or #fb_outputs > 0 or #fb_inouts > 0) then
|
||||
if fb_name then
|
||||
types[fb_name] = {
|
||||
name = fb_name,
|
||||
kind = "function_block",
|
||||
|
||||
Reference in New Issue
Block a user