docs: encourage adding comments; fix nested IF/ELSIF indentation
- Update AGENTS.md to encourage comments instead of forbidding them - Add docstrings to all public functions in src/ modules - Add module headers and function comments to lua/scl/ modules - Fix formatter to properly indent multi-line IF/ELSIF conditions - Fix FB call detection to not match IF statements with parentheses
This commit is contained in:
@@ -104,7 +104,7 @@ return M
|
|||||||
- No trailing whitespace
|
- No trailing whitespace
|
||||||
|
|
||||||
### Comments
|
### Comments
|
||||||
**DO NOT ADD COMMENTS** in code unless explicitly requested by the user.
|
Add comments to explain non-obvious code sections, complex logic, and public API functions. Keep comments concise and relevant.
|
||||||
|
|
||||||
### Parser Module API
|
### Parser Module API
|
||||||
All parser modules must implement:
|
All parser modules must implement:
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
-- TIA Portal built-in instructions for SCL LSP
|
||||||
|
-- Defines functions, function blocks, data types, and their parameters
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
-- Built-in functions (return values, no state)
|
||||||
M.FUNCTIONS = {
|
M.FUNCTIONS = {
|
||||||
ADD = true,
|
ADD = true,
|
||||||
SUB = true,
|
SUB = true,
|
||||||
@@ -169,6 +173,7 @@ M.FUNCTIONS = {
|
|||||||
DCP_CLIENT = true,
|
DCP_CLIENT = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Built-in function blocks (have state, need instance)
|
||||||
M.FUNCTION_BLOCKS = {
|
M.FUNCTION_BLOCKS = {
|
||||||
TON = true,
|
TON = true,
|
||||||
TOF = true,
|
TOF = true,
|
||||||
@@ -226,6 +231,8 @@ M.DATA_TYPES = {
|
|||||||
DTL = true,
|
DTL = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Parameter definitions for built-in instructions
|
||||||
|
-- Format: { inputs = { { name, type }, ... }, outputs = { { name, type }, ... } }
|
||||||
M.PARAMETERS = {
|
M.PARAMETERS = {
|
||||||
TON = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } },
|
TON = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } },
|
||||||
TOF = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } },
|
TOF = { inputs = { { name = "IN", type = "BOOL" }, { name = "PT", type = "TIME" } }, outputs = { { name = "Q", type = "BOOL" }, { name = "ET", type = "TIME" } } },
|
||||||
@@ -283,10 +290,48 @@ M.PARAMETERS = {
|
|||||||
I_BCD = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } },
|
I_BCD = { inputs = { { name = "IN" } }, outputs = { { name = "OUT" } } },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Get parameter info for a built-in instruction
|
||||||
|
-- @param name string The instruction name
|
||||||
|
-- @return table|nil Parameter definition with inputs and outputs arrays
|
||||||
function M.get_parameters(name)
|
function M.get_parameters(name)
|
||||||
return M.PARAMETERS[name:upper()]
|
return M.PARAMETERS[name:upper()]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Check if name is a known function block
|
||||||
|
-- @param name string The name to check
|
||||||
|
-- @return boolean True if known function block
|
||||||
|
function M.is_known_function_block(name)
|
||||||
|
return M.FUNCTION_BLOCKS[name:upper()] or false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if name is a known function
|
||||||
|
-- @param name string The name to check
|
||||||
|
-- @return boolean True if known function
|
||||||
|
function M.is_known_function(name)
|
||||||
|
return M.FUNCTIONS[name:upper()] or false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if name is a known data type
|
||||||
|
-- @param name string The name to check
|
||||||
|
-- @return boolean True if known data type
|
||||||
|
function M.is_known_data_type(name)
|
||||||
|
return M.DATA_TYPES[name:upper()] or false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if name is a known instruction (function or function block)
|
||||||
|
-- @param name string The name to check
|
||||||
|
-- @return boolean True if known instruction
|
||||||
|
function M.is_known_instruction(name)
|
||||||
|
return M.is_known_function(name) or M.is_known_function_block(name) or false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if name is a known type or instruction
|
||||||
|
-- @param name string The name to check
|
||||||
|
-- @return boolean True if known type or instruction
|
||||||
|
function M.is_known_type_or_instruction(name)
|
||||||
|
return M.is_known_data_type(name) or M.is_known_instruction(name) or false
|
||||||
|
end
|
||||||
|
|
||||||
function M.is_known_function(name)
|
function M.is_known_function(name)
|
||||||
return M.FUNCTIONS[name:upper()] or false
|
return M.FUNCTIONS[name:upper()] or false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
-- SCL Diagnostics - Linter functionality for the LSP server
|
-- SCL Diagnostics - Linter functionality for the LSP server
|
||||||
|
-- Validates SCL code for undefined variables, unknown types, and syntax errors
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
-- Get script path for loading parser (same pattern as main.lua)
|
-- Get script path for loading parser (same pattern as main.lua)
|
||||||
@@ -18,6 +20,7 @@ package.path = package.path .. ";" .. script_path .. "/?.lua"
|
|||||||
local parser = dofile(script_path .. "/parser.lua")
|
local parser = dofile(script_path .. "/parser.lua")
|
||||||
local builtin = dofile(script_path .. "/builtin_instructions.lua")
|
local builtin = dofile(script_path .. "/builtin_instructions.lua")
|
||||||
|
|
||||||
|
-- LSP diagnostic severity levels
|
||||||
local DiagnosticSeverity = {
|
local DiagnosticSeverity = {
|
||||||
Error = 1,
|
Error = 1,
|
||||||
Warning = 2,
|
Warning = 2,
|
||||||
@@ -25,6 +28,10 @@ local DiagnosticSeverity = {
|
|||||||
Hint = 4,
|
Hint = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--- Convert line/character to LSP position format
|
||||||
|
--- @param line number Zero-based line number
|
||||||
|
--- @param character number Zero-based character offset
|
||||||
|
--- @return table LSP Position object
|
||||||
local function position_to_lsp(line, character)
|
local function position_to_lsp(line, character)
|
||||||
return {
|
return {
|
||||||
line = line,
|
line = line,
|
||||||
@@ -32,6 +39,12 @@ local function position_to_lsp(line, character)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Create LSP Range object from start and end positions
|
||||||
|
--- @param start_line number Start line (zero-based)
|
||||||
|
--- @param start_char number Start character (zero-based)
|
||||||
|
--- @param end_line number End line (zero-based)
|
||||||
|
--- @param end_char number End character (zero-based)
|
||||||
|
--- @return table LSP Range object
|
||||||
local function range_to_lsp(start_line, start_char, end_line, end_char)
|
local function range_to_lsp(start_line, start_char, end_line, end_char)
|
||||||
return {
|
return {
|
||||||
start = position_to_lsp(start_line, start_char),
|
start = position_to_lsp(start_line, start_char),
|
||||||
@@ -39,6 +52,12 @@ local function range_to_lsp(start_line, start_char, end_line, end_char)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Validate SCL content and generate diagnostics
|
||||||
|
--- Checks for: undefined variables, invalid # prefix, unknown data types
|
||||||
|
--- @param content string The SCL source code to validate
|
||||||
|
--- @param workspace_types table Known types from workspace scanning
|
||||||
|
--- @param root_dir string Project root directory for additional type lookup
|
||||||
|
--- @return table Array of LSP Diagnostic objects
|
||||||
function M.validate_diagnostics(content, workspace_types, root_dir)
|
function M.validate_diagnostics(content, workspace_types, root_dir)
|
||||||
local diagnostics = {}
|
local diagnostics = {}
|
||||||
local lines = {}
|
local lines = {}
|
||||||
|
|||||||
+56
-15
@@ -1,5 +1,6 @@
|
|||||||
-- SCL Formatter - Document formatting for the LSP server
|
-- SCL Formatter - Document formatting for the LSP server
|
||||||
-- Stack-based implementation for proper nested structure handling
|
-- Stack-based implementation for proper nested structure handling
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
-- Default patterns for attribute blocks to collapse
|
-- Default patterns for attribute blocks to collapse
|
||||||
@@ -10,10 +11,20 @@ local DEFAULT_COLLAPSE_PATTERNS = {
|
|||||||
"^%s*{%s*%w+%s*:=",
|
"^%s*{%s*%w+%s*:=",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--- Create LSP Position object
|
||||||
|
--- @param line number Line number
|
||||||
|
--- @param character number Character offset
|
||||||
|
--- @return table Position object
|
||||||
local function position(line, character)
|
local function position(line, character)
|
||||||
return { line = line, character = character }
|
return { line = line, character = character }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Create LSP Range object
|
||||||
|
--- @param start_line number Start line
|
||||||
|
--- @param start_char number Start character
|
||||||
|
--- @param end_line number End line
|
||||||
|
--- @param end_char number End character
|
||||||
|
--- @return table Range object
|
||||||
local function range(start_line, start_char, end_line, end_char)
|
local function range(start_line, start_char, end_line, end_char)
|
||||||
return {
|
return {
|
||||||
start = position(start_line, start_char),
|
start = position(start_line, start_char),
|
||||||
@@ -89,6 +100,11 @@ local CONTINUATION_OPERATORS = {
|
|||||||
["MOD"] = true,
|
["MOD"] = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--- Format SCL document content
|
||||||
|
--- Handles indentation, attribute collapsing, and multiline FB calls
|
||||||
|
--- @param content string The SCL source code
|
||||||
|
--- @param options table Formatting options (insertSpaces, tabSize, collapseAttributes)
|
||||||
|
--- @return table Array of TextEdit objects with range and newText
|
||||||
function M.format_document(content, options)
|
function M.format_document(content, options)
|
||||||
options = options or {}
|
options = options or {}
|
||||||
local use_spaces = options.insertSpaces or false
|
local use_spaces = options.insertSpaces or false
|
||||||
@@ -147,6 +163,15 @@ function M.format_document(content, options)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function is_fb_call_start(trimmed)
|
local function is_fb_call_start(trimmed)
|
||||||
|
local first_word = trimmed:match("^(#?[%w_]+)")
|
||||||
|
if not first_word then return false end
|
||||||
|
|
||||||
|
-- Don't treat control structure keywords as FB calls
|
||||||
|
local control_keywords = { IF = true, ELSIF = true, FOR = true, WHILE = true, CASE = true, WITH = true }
|
||||||
|
if control_keywords[first_word:upper()] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
if not (trimmed:match("^#?%w+") or trimmed:match('^"')) then
|
if not (trimmed:match("^#?%w+") or trimmed:match('^"')) then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@@ -341,10 +366,14 @@ function M.format_document(content, options)
|
|||||||
|
|
||||||
-- Check if continuing condition (IF/ELSIF condition before THEN)
|
-- Check if continuing condition (IF/ELSIF condition before THEN)
|
||||||
if in_condition and not ends_assignment then
|
if in_condition and not ends_assignment then
|
||||||
local starts_with_operator = is_assignment_continuation(trimmed)
|
|
||||||
local first_word = trimmed:match("^([%w_]+)")
|
local first_word = trimmed:match("^([%w_]+)")
|
||||||
local is_then = first_word and first_word:upper() == "THEN"
|
local is_then = first_word and first_word:upper() == "THEN"
|
||||||
if starts_with_operator or is_then then
|
local is_else = first_word and first_word:upper() == "ELSE"
|
||||||
|
local is_elsif = first_word and first_word:upper() == "ELSIF"
|
||||||
|
local is_ender = trimmed:match("^END_") ~= nil
|
||||||
|
local starts_new_statement = is_then or is_else or is_elsif or is_ender
|
||||||
|
|
||||||
|
if not starts_new_statement then
|
||||||
table.insert(formatted_lines, condition_indent .. trimmed)
|
table.insert(formatted_lines, condition_indent .. trimmed)
|
||||||
if is_then then
|
if is_then then
|
||||||
in_condition = false
|
in_condition = false
|
||||||
@@ -474,19 +503,20 @@ function M.format_document(content, options)
|
|||||||
if SAME_LEVEL[kw] then
|
if SAME_LEVEL[kw] then
|
||||||
local formatted
|
local formatted
|
||||||
if kw == "THEN" then
|
if kw == "THEN" then
|
||||||
local if_block = nil
|
local then_indent
|
||||||
for i = #stack, 1, -1 do
|
if in_condition and condition_indent ~= "" then
|
||||||
if stack[i].type == "IF" then
|
then_indent = condition_indent
|
||||||
if_block = stack[i]
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if if_block then
|
|
||||||
formatted = string.rep(indent_char, if_block.indent) .. trimmed
|
|
||||||
else
|
else
|
||||||
formatted = get_indent() .. trimmed
|
local if_block = nil
|
||||||
|
for i = #stack, 1, -1 do
|
||||||
|
if stack[i].type == "IF" then
|
||||||
|
if_block = stack[i]
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
then_indent = if_block and string.rep(indent_char, if_block.indent) or get_indent()
|
||||||
end
|
end
|
||||||
table.insert(formatted_lines, formatted)
|
table.insert(formatted_lines, then_indent .. trimmed)
|
||||||
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
||||||
in_condition = false
|
in_condition = false
|
||||||
elseif kw == "ELSE" or kw == "ELSIF" then
|
elseif kw == "ELSE" or kw == "ELSIF" then
|
||||||
@@ -507,6 +537,11 @@ function M.format_document(content, options)
|
|||||||
end
|
end
|
||||||
table.insert(formatted_lines, formatted)
|
table.insert(formatted_lines, formatted)
|
||||||
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
||||||
|
-- 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)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
formatted = get_indent() .. trimmed
|
formatted = get_indent() .. trimmed
|
||||||
table.insert(formatted_lines, formatted)
|
table.insert(formatted_lines, formatted)
|
||||||
@@ -540,8 +575,12 @@ function M.format_document(content, options)
|
|||||||
table.insert(formatted_lines, formatted)
|
table.insert(formatted_lines, formatted)
|
||||||
table.insert(stack, { type = kw, indent = current_level })
|
table.insert(stack, { type = kw, indent = current_level })
|
||||||
if kw == "IF" or kw == "ELSIF" then
|
if kw == "IF" or kw == "ELSIF" then
|
||||||
in_condition = true
|
-- Check if this is a multi-line condition (doesn't end with THEN on same line)
|
||||||
condition_indent = string.rep(indent_char, current_level)
|
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)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
goto continue
|
goto continue
|
||||||
end
|
end
|
||||||
@@ -587,6 +626,8 @@ function M.format_document(content, options)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get default formatting options
|
||||||
|
--- @return table Default options table
|
||||||
function M.get_formatting_options()
|
function M.get_formatting_options()
|
||||||
return {
|
return {
|
||||||
insertSpaces = false,
|
insertSpaces = false,
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
-- JSON Encoder/Decoder for LSP server
|
||||||
|
-- Minimal implementation supporting basic JSON parsing and encoding
|
||||||
|
|
||||||
local json = {}
|
local json = {}
|
||||||
|
|
||||||
|
--- Check if a table is an array (sequential numeric keys starting from 1)
|
||||||
|
--- @param t table Table to check
|
||||||
|
--- @return boolean True if table is an array
|
||||||
local function is_array(t)
|
local function is_array(t)
|
||||||
local i = 0
|
local i = 0
|
||||||
for _ in pairs(t) do
|
for _ in pairs(t) do
|
||||||
@@ -11,6 +17,9 @@ local function is_array(t)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Escape special characters in string for JSON
|
||||||
|
--- @param s string String to escape
|
||||||
|
--- @return string Escaped string
|
||||||
local function escape(s)
|
local function escape(s)
|
||||||
s = s:gsub("\\", "\\\\")
|
s = s:gsub("\\", "\\\\")
|
||||||
s = s:gsub('"', '\\"')
|
s = s:gsub('"', '\\"')
|
||||||
@@ -20,6 +29,9 @@ local function escape(s)
|
|||||||
return s
|
return s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Encode Lua value to JSON string
|
||||||
|
--- @param data any Value to encode
|
||||||
|
--- @return string JSON string
|
||||||
function json.encode(data)
|
function json.encode(data)
|
||||||
local t = type(data)
|
local t = type(data)
|
||||||
if t == "nil" then
|
if t == "nil" then
|
||||||
@@ -48,6 +60,10 @@ function json.encode(data)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Skip whitespace in string
|
||||||
|
--- @param s string JSON string
|
||||||
|
--- @param i number Current position
|
||||||
|
--- @return number New position after whitespace
|
||||||
local function skip_whitespace(s, i)
|
local function skip_whitespace(s, i)
|
||||||
while i <= #s and s:sub(i, i):match("%s") do
|
while i <= #s and s:sub(i, i):match("%s") do
|
||||||
i = i + 1
|
i = i + 1
|
||||||
@@ -55,6 +71,11 @@ local function skip_whitespace(s, i)
|
|||||||
return i
|
return i
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Parse JSON string literal
|
||||||
|
--- @param s string JSON string
|
||||||
|
--- @param i number Current position (at opening quote)
|
||||||
|
--- @return string|nil Parsed string
|
||||||
|
--- @return number New position
|
||||||
local function parse_string(s, i)
|
local function parse_string(s, i)
|
||||||
local result = {}
|
local result = {}
|
||||||
i = i + 1
|
i = i + 1
|
||||||
@@ -86,6 +107,11 @@ local function parse_string(s, i)
|
|||||||
return nil, i
|
return nil, i
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Parse JSON number
|
||||||
|
--- @param s string JSON string
|
||||||
|
--- @param i number Current position
|
||||||
|
--- @return number Parsed number
|
||||||
|
--- @return number New position
|
||||||
local function parse_number(s, i)
|
local function parse_number(s, i)
|
||||||
local start = i
|
local start = i
|
||||||
if s:sub(i, i) == "-" then
|
if s:sub(i, i) == "-" then
|
||||||
@@ -114,6 +140,11 @@ end
|
|||||||
|
|
||||||
local parse_value
|
local parse_value
|
||||||
|
|
||||||
|
--- Parse JSON array
|
||||||
|
--- @param s string JSON string
|
||||||
|
--- @param i number Current position (at opening bracket)
|
||||||
|
--- @return table|nil Parsed array
|
||||||
|
--- @return number New position
|
||||||
local function parse_array(s, i)
|
local function parse_array(s, i)
|
||||||
local arr = {}
|
local arr = {}
|
||||||
i = i + 1
|
i = i + 1
|
||||||
@@ -136,6 +167,11 @@ local function parse_array(s, i)
|
|||||||
return nil, i
|
return nil, i
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Parse JSON object
|
||||||
|
--- @param s string JSON string
|
||||||
|
--- @param i number Current position (at opening brace)
|
||||||
|
--- @return table|nil Parsed object
|
||||||
|
--- @return number New position
|
||||||
local function parse_object(s, i)
|
local function parse_object(s, i)
|
||||||
local obj = {}
|
local obj = {}
|
||||||
i = i + 1
|
i = i + 1
|
||||||
@@ -205,6 +241,10 @@ parse_value = function(s, i)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Decode JSON string to Lua value
|
||||||
|
--- @param s string JSON string
|
||||||
|
--- @return any|nil Parsed value
|
||||||
|
--- @return number|nil Position after parsing or error
|
||||||
function json.decode(s)
|
function json.decode(s)
|
||||||
if type(s) ~= "string" then
|
if type(s) ~= "string" then
|
||||||
return nil, "invalid input"
|
return nil, "invalid input"
|
||||||
|
|||||||
@@ -154,6 +154,9 @@ end
|
|||||||
|
|
||||||
local handlers = {}
|
local handlers = {}
|
||||||
|
|
||||||
|
--- Handle LSP initialize request
|
||||||
|
--- @param params table Initialization parameters from client
|
||||||
|
--- @return table Server capabilities and info
|
||||||
function handlers.initialize(params)
|
function handlers.initialize(params)
|
||||||
return {
|
return {
|
||||||
capabilities = capabilities,
|
capabilities = capabilities,
|
||||||
@@ -161,8 +164,13 @@ function handlers.initialize(params)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle initialized notification (no response needed)
|
||||||
|
--- @param params table Parameters (usually empty)
|
||||||
function handlers.initialized(params) end
|
function handlers.initialized(params) end
|
||||||
|
|
||||||
|
--- Handle shutdown request
|
||||||
|
--- @param params table Parameters (usually empty)
|
||||||
|
--- @return nil
|
||||||
function handlers.shutdown(params)
|
function handlers.shutdown(params)
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -206,10 +214,15 @@ local function find_project_root(start_path)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle exit notification - terminates the server
|
||||||
|
--- @param params table Parameters (usually empty)
|
||||||
function handlers.exit(params)
|
function handlers.exit(params)
|
||||||
os.exit(0)
|
os.exit(0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle textDocument/didOpen notification
|
||||||
|
--- Parses document content, extracts symbols, and stores in documents cache
|
||||||
|
--- @param params table Contains textDocument with uri, text, version
|
||||||
function handlers.textDocument_didOpen(params)
|
function handlers.textDocument_didOpen(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local content = params.textDocument.text
|
local content = params.textDocument.text
|
||||||
@@ -247,6 +260,9 @@ function handlers.textDocument_didOpen(params)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle textDocument/didChange notification
|
||||||
|
--- Re-parses document content after edits
|
||||||
|
--- @param params table Contains textDocument with uri and contentChanges array
|
||||||
function handlers.textDocument_didChange(params)
|
function handlers.textDocument_didChange(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local content = params.contentChanges[1].text
|
local content = params.contentChanges[1].text
|
||||||
@@ -285,11 +301,18 @@ function handlers.textDocument_didChange(params)
|
|||||||
clear_symbol_cache()
|
clear_symbol_cache()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle textDocument/didClose notification
|
||||||
|
--- Removes document from cache
|
||||||
|
--- @param params table Contains textDocument with uri
|
||||||
function handlers.textDocument_didClose(params)
|
function handlers.textDocument_didClose(params)
|
||||||
documents[params.textDocument.uri] = nil
|
documents[params.textDocument.uri] = nil
|
||||||
clear_symbol_cache()
|
clear_symbol_cache()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle textDocument/hover request
|
||||||
|
--- Provides hover information for variables, types, and built-in instructions
|
||||||
|
--- @param params table Contains textDocument with uri and position
|
||||||
|
--- @return table|nil Hover result with contents and range, or nil
|
||||||
function handlers.textDocument_hover(params)
|
function handlers.textDocument_hover(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
@@ -699,6 +722,10 @@ function handlers.textDocument_hover(params)
|
|||||||
return nil
|
return nil
|
||||||
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
|
||||||
|
--- @return table Completion list with items array
|
||||||
function handlers.textDocument_completion(params)
|
function handlers.textDocument_completion(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
@@ -879,6 +906,10 @@ function handlers.textDocument_completion(params)
|
|||||||
return { isIncomplete = false, items = items }
|
return { isIncomplete = false, items = items }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle textDocument/definition request
|
||||||
|
--- Finds definition location for symbol at position
|
||||||
|
--- @param params table Contains textDocument with uri and position
|
||||||
|
--- @return table|nil Location with uri and range, or nil
|
||||||
function handlers.textDocument_definition(params)
|
function handlers.textDocument_definition(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
@@ -921,6 +952,10 @@ function handlers.textDocument_definition(params)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle textDocument/declaration request
|
||||||
|
--- Finds declaration locations for symbol at position across workspace
|
||||||
|
--- @param params table Contains textDocument with uri and position
|
||||||
|
--- @return table|nil Array of locations, or nil
|
||||||
function handlers.textDocument_declaration(params)
|
function handlers.textDocument_declaration(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
@@ -1141,6 +1176,10 @@ function handlers.textDocument_declaration(params)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle textDocument/references request
|
||||||
|
--- Finds all references to symbol at position in current document
|
||||||
|
--- @param params table Contains textDocument with uri and position
|
||||||
|
--- @return table|nil Array of reference locations, or nil
|
||||||
function handlers.textDocument_references(params)
|
function handlers.textDocument_references(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
@@ -1202,6 +1241,10 @@ function handlers.textDocument_references(params)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle textDocument/documentSymbol request
|
||||||
|
--- Returns all symbols in document for outline/navigation
|
||||||
|
--- @param params table Contains textDocument with uri
|
||||||
|
--- @return table Array of DocumentSymbol objects
|
||||||
function handlers.textDocument_documentSymbol(params)
|
function handlers.textDocument_documentSymbol(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
@@ -1286,6 +1329,10 @@ function handlers.textDocument_documentSymbol(params)
|
|||||||
return symbols
|
return symbols
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle textDocument/diagnostic request
|
||||||
|
--- Validates document and returns diagnostics
|
||||||
|
--- @param params table Contains textDocument with uri
|
||||||
|
--- @return table Diagnostic report with items array
|
||||||
function handlers.textDocument_diagnostic(params)
|
function handlers.textDocument_diagnostic(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
@@ -1302,6 +1349,10 @@ function handlers.textDocument_diagnostic(params)
|
|||||||
return { items = diag_results }
|
return { items = diag_results }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle textDocument/semanticTokens/full request
|
||||||
|
--- Returns semantic tokens for syntax highlighting (currently disabled)
|
||||||
|
--- @param params table Contains textDocument with uri
|
||||||
|
--- @return table Semantic tokens with data array
|
||||||
function handlers.textDocument_semanticTokensFull(params)
|
function handlers.textDocument_semanticTokensFull(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
@@ -1399,6 +1450,10 @@ function handlers.textDocument_semanticTokensFull(params)
|
|||||||
return { data = tokens }
|
return { data = tokens }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle textDocument/semanticTokens/range request
|
||||||
|
--- Returns semantic tokens for a range (delegates to full)
|
||||||
|
--- @param params table Contains textDocument with uri and range
|
||||||
|
--- @return table Semantic tokens with data array
|
||||||
function handlers.textDocument_semanticTokensRange(params)
|
function handlers.textDocument_semanticTokensRange(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
@@ -1412,6 +1467,10 @@ end
|
|||||||
handlers["textDocument_semanticTokens_full"] = handlers.textDocument_semanticTokensFull
|
handlers["textDocument_semanticTokens_full"] = handlers.textDocument_semanticTokensFull
|
||||||
handlers["textDocument_semanticTokens_range"] = handlers.textDocument_semanticTokensRange
|
handlers["textDocument_semanticTokens_range"] = handlers.textDocument_semanticTokensRange
|
||||||
|
|
||||||
|
--- Handle textDocument/inlayHint request
|
||||||
|
--- Provides inlay hints for variable types in VAR sections
|
||||||
|
--- @param params table Contains textDocument with uri and range
|
||||||
|
--- @return table Array of InlayHint objects
|
||||||
function handlers.textDocument_inlayHint(params)
|
function handlers.textDocument_inlayHint(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
@@ -1449,6 +1508,10 @@ function handlers.textDocument_inlayHint(params)
|
|||||||
return hints
|
return hints
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle workspace/symbol request
|
||||||
|
--- Searches workspace for symbols matching query
|
||||||
|
--- @param params table Contains query string and optional maxResults
|
||||||
|
--- @return table Array of SymbolInformation objects
|
||||||
function handlers.workspace_symbol(params)
|
function handlers.workspace_symbol(params)
|
||||||
local query = params.query or ""
|
local query = params.query or ""
|
||||||
local max_results = params.maxResults or 50
|
local max_results = params.maxResults or 50
|
||||||
@@ -1586,6 +1649,10 @@ function handlers.workspace_symbol(params)
|
|||||||
return final_results
|
return final_results
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle textDocument/formatting request
|
||||||
|
--- Formats entire document using SCL formatter
|
||||||
|
--- @param params table Contains textDocument with uri and options
|
||||||
|
--- @return table|nil Array of TextEdit objects, or nil
|
||||||
function handlers.textDocument_formatting(params)
|
function handlers.textDocument_formatting(params)
|
||||||
local uri = params.textDocument.uri
|
local uri = params.textDocument.uri
|
||||||
local doc = documents[uri]
|
local doc = documents[uri]
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
-- SCL Parser utilities for the LSP server
|
-- SCL Parser utilities for the LSP server
|
||||||
|
-- Extracts variables, types, and function definitions from SCL source code
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
--- Extract variables from SCL content
|
||||||
|
--- @param content string The SCL source code
|
||||||
|
--- @return table variables Table of variable definitions keyed by name
|
||||||
|
--- @return table variable_positions Table of variable positions keyed by name
|
||||||
function M.extract_variables(content)
|
function M.extract_variables(content)
|
||||||
local variables = {}
|
local variables = {}
|
||||||
local variable_positions = {}
|
local variable_positions = {}
|
||||||
@@ -164,6 +170,9 @@ function M.extract_variables(content)
|
|||||||
return variables, variable_positions
|
return variables, variable_positions
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Extract type definitions from SCL content
|
||||||
|
--- @param content string The SCL source code
|
||||||
|
--- @return table types Table of type definitions keyed by name
|
||||||
function M.extract_types(content)
|
function M.extract_types(content)
|
||||||
local types = {}
|
local types = {}
|
||||||
|
|
||||||
@@ -285,6 +294,9 @@ function M.extract_types(content)
|
|||||||
return types
|
return types
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Extract function/block definitions from SCL content
|
||||||
|
--- @param content string The SCL source code
|
||||||
|
--- @return table functions Array of function definitions with name, kind, start, final
|
||||||
function M.extract_functions(content)
|
function M.extract_functions(content)
|
||||||
local functions = {}
|
local functions = {}
|
||||||
|
|
||||||
|
|||||||
+33
-1
@@ -1,10 +1,14 @@
|
|||||||
-- Load UDT types from external .plc.json files
|
-- Load UDT types from external .plc.json files
|
||||||
-- Auto-generate from data_types folder
|
-- Auto-generate from data_types folder and .scl/.udt/.db files
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
-- Cache for loaded types: { [root_dir] = { [type_name] = type_info } }
|
||||||
local cached_types = {}
|
local cached_types = {}
|
||||||
|
|
||||||
|
--- Execute shell command and capture output
|
||||||
|
--- @param cmd string Command to execute
|
||||||
|
--- @return string|nil Command output, or nil on error
|
||||||
local function run_command(cmd)
|
local function run_command(cmd)
|
||||||
local handle = io.popen(cmd)
|
local handle = io.popen(cmd)
|
||||||
if not handle then return nil end
|
if not handle then return nil end
|
||||||
@@ -26,6 +30,9 @@ else
|
|||||||
script_path = "."
|
script_path = "."
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Load and parse JSON content
|
||||||
|
--- @param content string JSON string
|
||||||
|
--- @return table|nil Parsed data, or nil on error
|
||||||
local function load_json(content)
|
local function load_json(content)
|
||||||
local json = dofile(script_path .. "/json.lua")
|
local json = dofile(script_path .. "/json.lua")
|
||||||
local ok, data = pcall(function() return json.decode(content) end)
|
local ok, data = pcall(function() return json.decode(content) end)
|
||||||
@@ -33,6 +40,10 @@ local function load_json(content)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Scan directory recursively for files matching pattern
|
||||||
|
--- @param dir string Directory to scan
|
||||||
|
--- @param pattern string Glob pattern for file names
|
||||||
|
--- @return table Array of matching file paths
|
||||||
local function scan_files_recursive(dir, pattern)
|
local function scan_files_recursive(dir, pattern)
|
||||||
local files = {}
|
local files = {}
|
||||||
local result = run_command("find " .. dir .. " -type f -name \"" .. pattern .. "\" 2>/dev/null")
|
local result = run_command("find " .. dir .. " -type f -name \"" .. pattern .. "\" 2>/dev/null")
|
||||||
@@ -44,6 +55,9 @@ local function scan_files_recursive(dir, pattern)
|
|||||||
return files
|
return files
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Parse a PLC type definition from JSON structure
|
||||||
|
--- @param dt table Type definition from JSON
|
||||||
|
--- @return table|nil Parsed type info, or nil if invalid
|
||||||
local function parse_plc_type(dt)
|
local function parse_plc_type(dt)
|
||||||
local type_name = dt.name or dt.Name or dt.baseType or dt.dataTypeName
|
local type_name = dt.name or dt.Name or dt.baseType or dt.dataTypeName
|
||||||
if not type_name then return nil end
|
if not type_name then return nil end
|
||||||
@@ -78,6 +92,9 @@ local function parse_plc_type(dt)
|
|||||||
return typ
|
return typ
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Parse SCL type file content (TYPE...END_TYPE, FUNCTION_BLOCK, etc.)
|
||||||
|
--- @param content string SCL source code
|
||||||
|
--- @return table types Parsed types keyed by name
|
||||||
local function parse_scl_type_file(content)
|
local function parse_scl_type_file(content)
|
||||||
local types = {}
|
local types = {}
|
||||||
local in_type = false
|
local in_type = false
|
||||||
@@ -244,6 +261,9 @@ local function parse_scl_type_file(content)
|
|||||||
return types
|
return types
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Parse DB file content (DATA_BLOCK...END_DATA_BLOCK)
|
||||||
|
--- @param content string DB source code
|
||||||
|
--- @return table types Parsed DB types keyed by name
|
||||||
local function parse_db_file(content)
|
local function parse_db_file(content)
|
||||||
local types = {}
|
local types = {}
|
||||||
local in_var = false
|
local in_var = false
|
||||||
@@ -288,6 +308,10 @@ local function parse_db_file(content)
|
|||||||
return types
|
return types
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Load all types from workspace directory
|
||||||
|
--- Scans .plc.json, .scl, .udt, and .db files
|
||||||
|
--- @param root_dir string Project root directory
|
||||||
|
--- @return table Types keyed by name
|
||||||
function M.load_types_from_workspace(root_dir)
|
function M.load_types_from_workspace(root_dir)
|
||||||
local types = {}
|
local types = {}
|
||||||
|
|
||||||
@@ -375,6 +399,9 @@ function M.load_types_from_workspace(root_dir)
|
|||||||
return types
|
return types
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get types for a workspace (cached)
|
||||||
|
--- @param root_dir string Project root directory
|
||||||
|
--- @return table Types keyed by name
|
||||||
function M.get_types(root_dir)
|
function M.get_types(root_dir)
|
||||||
if not root_dir or root_dir == "" then
|
if not root_dir or root_dir == "" then
|
||||||
root_dir = "workspace"
|
root_dir = "workspace"
|
||||||
@@ -389,6 +416,10 @@ function M.get_types(root_dir)
|
|||||||
return types
|
return types
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Generate .plc.json file from workspace types
|
||||||
|
--- @param root_dir string Project root directory
|
||||||
|
--- @param output_file string Output file path
|
||||||
|
--- @return boolean True if file was written successfully
|
||||||
function M.generate_plc_json(root_dir, output_file)
|
function M.generate_plc_json(root_dir, output_file)
|
||||||
local json = dofile(script_path .. "/json.lua")
|
local json = dofile(script_path .. "/json.lua")
|
||||||
|
|
||||||
@@ -435,6 +466,7 @@ function M.generate_plc_json(root_dir, output_file)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Clear all cached types
|
||||||
function M.clear_cache()
|
function M.clear_cache()
|
||||||
cached_types = {}
|
cached_types = {}
|
||||||
end
|
end
|
||||||
|
|||||||
+28
-1
@@ -1,10 +1,13 @@
|
|||||||
-- Tree-sitter integration for SCL LSP
|
-- Tree-sitter integration for SCL LSP
|
||||||
-- Uses tree-sitter to parse SCL files more accurately
|
-- Uses tree-sitter to parse SCL files more accurately than regex parsing
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local ts = nil
|
local ts = nil
|
||||||
|
|
||||||
|
--- Initialize tree-sitter parser
|
||||||
|
--- Attempts to load vim.treesitter module
|
||||||
|
--- @return boolean True if tree-sitter is available
|
||||||
function M.init()
|
function M.init()
|
||||||
local ok, treesitter = pcall(require, "vim.treesitter")
|
local ok, treesitter = pcall(require, "vim.treesitter")
|
||||||
if ok then
|
if ok then
|
||||||
@@ -14,6 +17,10 @@ function M.init()
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Parse SCL content using tree-sitter
|
||||||
|
--- @param content string The SCL source code
|
||||||
|
--- @return table|nil Parse tree, or nil on error
|
||||||
|
--- @return string|nil Error message if parsing failed
|
||||||
function M.parse_scl(content)
|
function M.parse_scl(content)
|
||||||
if not ts then
|
if not ts then
|
||||||
return nil, "Tree-sitter not available"
|
return nil, "Tree-sitter not available"
|
||||||
@@ -32,6 +39,11 @@ function M.parse_scl(content)
|
|||||||
return tree, nil
|
return tree, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Extract variables from tree-sitter parse tree
|
||||||
|
--- @param tree table Parse tree from parse_scl
|
||||||
|
--- @param bufnr number Buffer number for text retrieval
|
||||||
|
--- @return table variables Table of variable definitions
|
||||||
|
--- @return table variable_positions Table of variable positions
|
||||||
function M.extract_variables_ts(tree, bufnr)
|
function M.extract_variables_ts(tree, bufnr)
|
||||||
local variables = {}
|
local variables = {}
|
||||||
local variable_positions = {}
|
local variable_positions = {}
|
||||||
@@ -79,6 +91,10 @@ function M.extract_variables_ts(tree, bufnr)
|
|||||||
return variables, variable_positions
|
return variables, variable_positions
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Extract function/block definitions from tree-sitter parse tree
|
||||||
|
--- @param tree table Parse tree from parse_scl
|
||||||
|
--- @param bufnr number Buffer number for text retrieval
|
||||||
|
--- @return table functions Array of function definitions
|
||||||
function M.extract_functions_ts(tree, bufnr)
|
function M.extract_functions_ts(tree, bufnr)
|
||||||
local functions = {}
|
local functions = {}
|
||||||
|
|
||||||
@@ -109,6 +125,11 @@ function M.extract_functions_ts(tree, bufnr)
|
|||||||
return functions
|
return functions
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get syntax node at position
|
||||||
|
--- @param tree table Parse tree
|
||||||
|
--- @param row number Row number (zero-based)
|
||||||
|
--- @param col number Column number (zero-based)
|
||||||
|
--- @return table|nil Node at position, or nil if not found
|
||||||
function M.get_node_at_position(tree, row, col)
|
function M.get_node_at_position(tree, row, col)
|
||||||
if not tree then return nil end
|
if not tree then return nil end
|
||||||
|
|
||||||
@@ -124,6 +145,12 @@ function M.get_node_at_position(tree, row, col)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get variable type at cursor position
|
||||||
|
--- @param tree table Parse tree
|
||||||
|
--- @param bufnr number Buffer number
|
||||||
|
--- @param row number Row number (zero-based)
|
||||||
|
--- @param col number Column number (zero-based)
|
||||||
|
--- @return string|nil Variable type, or nil if not found
|
||||||
function M.get_variable_type_at_cursor(tree, bufnr, row, col)
|
function M.get_variable_type_at_cursor(tree, bufnr, row, col)
|
||||||
if not tree or not ts then return nil end
|
if not tree or not ts then return nil end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user