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
|
||||
|
||||
### 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
|
||||
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 = {}
|
||||
|
||||
-- Built-in functions (return values, no state)
|
||||
M.FUNCTIONS = {
|
||||
ADD = true,
|
||||
SUB = true,
|
||||
@@ -169,6 +173,7 @@ M.FUNCTIONS = {
|
||||
DCP_CLIENT = true,
|
||||
}
|
||||
|
||||
-- Built-in function blocks (have state, need instance)
|
||||
M.FUNCTION_BLOCKS = {
|
||||
TON = true,
|
||||
TOF = true,
|
||||
@@ -226,6 +231,8 @@ M.DATA_TYPES = {
|
||||
DTL = true,
|
||||
}
|
||||
|
||||
-- Parameter definitions for built-in instructions
|
||||
-- Format: { inputs = { { name, type }, ... }, outputs = { { name, type }, ... } }
|
||||
M.PARAMETERS = {
|
||||
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" } } },
|
||||
@@ -283,10 +290,48 @@ M.PARAMETERS = {
|
||||
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)
|
||||
return M.PARAMETERS[name:upper()]
|
||||
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)
|
||||
return M.FUNCTIONS[name:upper()] or false
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
-- SCL Diagnostics - Linter functionality for the LSP server
|
||||
-- Validates SCL code for undefined variables, unknown types, and syntax errors
|
||||
|
||||
local M = {}
|
||||
|
||||
-- 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 builtin = dofile(script_path .. "/builtin_instructions.lua")
|
||||
|
||||
-- LSP diagnostic severity levels
|
||||
local DiagnosticSeverity = {
|
||||
Error = 1,
|
||||
Warning = 2,
|
||||
@@ -25,6 +28,10 @@ local DiagnosticSeverity = {
|
||||
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)
|
||||
return {
|
||||
line = line,
|
||||
@@ -32,6 +39,12 @@ local function position_to_lsp(line, character)
|
||||
}
|
||||
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)
|
||||
return {
|
||||
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
|
||||
|
||||
--- 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)
|
||||
local diagnostics = {}
|
||||
local lines = {}
|
||||
|
||||
+49
-8
@@ -1,5 +1,6 @@
|
||||
-- SCL Formatter - Document formatting for the LSP server
|
||||
-- Stack-based implementation for proper nested structure handling
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Default patterns for attribute blocks to collapse
|
||||
@@ -10,10 +11,20 @@ local DEFAULT_COLLAPSE_PATTERNS = {
|
||||
"^%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)
|
||||
return { line = line, character = character }
|
||||
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)
|
||||
return {
|
||||
start = position(start_line, start_char),
|
||||
@@ -89,6 +100,11 @@ local CONTINUATION_OPERATORS = {
|
||||
["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)
|
||||
options = options or {}
|
||||
local use_spaces = options.insertSpaces or false
|
||||
@@ -147,6 +163,15 @@ function M.format_document(content, options)
|
||||
end
|
||||
|
||||
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
|
||||
return false
|
||||
end
|
||||
@@ -341,10 +366,14 @@ function M.format_document(content, options)
|
||||
|
||||
-- Check if continuing condition (IF/ELSIF condition before THEN)
|
||||
if in_condition and not ends_assignment then
|
||||
local starts_with_operator = is_assignment_continuation(trimmed)
|
||||
local first_word = trimmed:match("^([%w_]+)")
|
||||
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)
|
||||
if is_then then
|
||||
in_condition = false
|
||||
@@ -474,6 +503,10 @@ function M.format_document(content, options)
|
||||
if SAME_LEVEL[kw] then
|
||||
local formatted
|
||||
if kw == "THEN" then
|
||||
local then_indent
|
||||
if in_condition and condition_indent ~= "" then
|
||||
then_indent = condition_indent
|
||||
else
|
||||
local if_block = nil
|
||||
for i = #stack, 1, -1 do
|
||||
if stack[i].type == "IF" then
|
||||
@@ -481,12 +514,9 @@ function M.format_document(content, options)
|
||||
break
|
||||
end
|
||||
end
|
||||
if if_block then
|
||||
formatted = string.rep(indent_char, if_block.indent) .. trimmed
|
||||
else
|
||||
formatted = get_indent() .. trimmed
|
||||
then_indent = if_block and string.rep(indent_char, if_block.indent) or get_indent()
|
||||
end
|
||||
table.insert(formatted_lines, formatted)
|
||||
table.insert(formatted_lines, then_indent .. trimmed)
|
||||
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
||||
in_condition = false
|
||||
elseif kw == "ELSE" or kw == "ELSIF" then
|
||||
@@ -507,6 +537,11 @@ function M.format_document(content, options)
|
||||
end
|
||||
table.insert(formatted_lines, formatted)
|
||||
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
|
||||
formatted = get_indent() .. trimmed
|
||||
table.insert(formatted_lines, formatted)
|
||||
@@ -540,8 +575,12 @@ function M.format_document(content, options)
|
||||
table.insert(formatted_lines, formatted)
|
||||
table.insert(stack, { type = kw, indent = current_level })
|
||||
if kw == "IF" or kw == "ELSIF" then
|
||||
-- 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, current_level)
|
||||
-- Continuation lines should be indented one level deeper than IF/ELSIF
|
||||
condition_indent = string.rep(indent_char, current_level + 1)
|
||||
end
|
||||
end
|
||||
goto continue
|
||||
end
|
||||
@@ -587,6 +626,8 @@ function M.format_document(content, options)
|
||||
}
|
||||
end
|
||||
|
||||
--- Get default formatting options
|
||||
--- @return table Default options table
|
||||
function M.get_formatting_options()
|
||||
return {
|
||||
insertSpaces = false,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
-- JSON Encoder/Decoder for LSP server
|
||||
-- Minimal implementation supporting basic JSON parsing and encoding
|
||||
|
||||
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 i = 0
|
||||
for _ in pairs(t) do
|
||||
@@ -11,6 +17,9 @@ local function is_array(t)
|
||||
return true
|
||||
end
|
||||
|
||||
--- Escape special characters in string for JSON
|
||||
--- @param s string String to escape
|
||||
--- @return string Escaped string
|
||||
local function escape(s)
|
||||
s = s:gsub("\\", "\\\\")
|
||||
s = s:gsub('"', '\\"')
|
||||
@@ -20,6 +29,9 @@ local function escape(s)
|
||||
return s
|
||||
end
|
||||
|
||||
--- Encode Lua value to JSON string
|
||||
--- @param data any Value to encode
|
||||
--- @return string JSON string
|
||||
function json.encode(data)
|
||||
local t = type(data)
|
||||
if t == "nil" then
|
||||
@@ -48,6 +60,10 @@ function json.encode(data)
|
||||
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)
|
||||
while i <= #s and s:sub(i, i):match("%s") do
|
||||
i = i + 1
|
||||
@@ -55,6 +71,11 @@ local function skip_whitespace(s, i)
|
||||
return i
|
||||
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 result = {}
|
||||
i = i + 1
|
||||
@@ -86,6 +107,11 @@ local function parse_string(s, i)
|
||||
return nil, i
|
||||
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 start = i
|
||||
if s:sub(i, i) == "-" then
|
||||
@@ -114,6 +140,11 @@ end
|
||||
|
||||
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 arr = {}
|
||||
i = i + 1
|
||||
@@ -136,6 +167,11 @@ local function parse_array(s, i)
|
||||
return nil, i
|
||||
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 obj = {}
|
||||
i = i + 1
|
||||
@@ -205,6 +241,10 @@ parse_value = function(s, i)
|
||||
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)
|
||||
if type(s) ~= "string" then
|
||||
return nil, "invalid input"
|
||||
|
||||
@@ -154,6 +154,9 @@ end
|
||||
|
||||
local handlers = {}
|
||||
|
||||
--- Handle LSP initialize request
|
||||
--- @param params table Initialization parameters from client
|
||||
--- @return table Server capabilities and info
|
||||
function handlers.initialize(params)
|
||||
return {
|
||||
capabilities = capabilities,
|
||||
@@ -161,8 +164,13 @@ function handlers.initialize(params)
|
||||
}
|
||||
end
|
||||
|
||||
--- Handle initialized notification (no response needed)
|
||||
--- @param params table Parameters (usually empty)
|
||||
function handlers.initialized(params) end
|
||||
|
||||
--- Handle shutdown request
|
||||
--- @param params table Parameters (usually empty)
|
||||
--- @return nil
|
||||
function handlers.shutdown(params)
|
||||
return nil
|
||||
end
|
||||
@@ -206,10 +214,15 @@ local function find_project_root(start_path)
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Handle exit notification - terminates the server
|
||||
--- @param params table Parameters (usually empty)
|
||||
function handlers.exit(params)
|
||||
os.exit(0)
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local content = params.textDocument.text
|
||||
@@ -247,6 +260,9 @@ function handlers.textDocument_didOpen(params)
|
||||
}
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local content = params.contentChanges[1].text
|
||||
@@ -285,11 +301,18 @@ function handlers.textDocument_didChange(params)
|
||||
clear_symbol_cache()
|
||||
end
|
||||
|
||||
--- Handle textDocument/didClose notification
|
||||
--- Removes document from cache
|
||||
--- @param params table Contains textDocument with uri
|
||||
function handlers.textDocument_didClose(params)
|
||||
documents[params.textDocument.uri] = nil
|
||||
clear_symbol_cache()
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
@@ -699,6 +722,10 @@ function handlers.textDocument_hover(params)
|
||||
return nil
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
@@ -879,6 +906,10 @@ function handlers.textDocument_completion(params)
|
||||
return { isIncomplete = false, items = items }
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
@@ -921,6 +952,10 @@ function handlers.textDocument_definition(params)
|
||||
return nil
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
@@ -1141,6 +1176,10 @@ function handlers.textDocument_declaration(params)
|
||||
return nil
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
@@ -1202,6 +1241,10 @@ function handlers.textDocument_references(params)
|
||||
return nil
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
@@ -1286,6 +1329,10 @@ function handlers.textDocument_documentSymbol(params)
|
||||
return symbols
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
@@ -1302,6 +1349,10 @@ function handlers.textDocument_diagnostic(params)
|
||||
return { items = diag_results }
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
@@ -1399,6 +1450,10 @@ function handlers.textDocument_semanticTokensFull(params)
|
||||
return { data = tokens }
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
@@ -1412,6 +1467,10 @@ end
|
||||
handlers["textDocument_semanticTokens_full"] = handlers.textDocument_semanticTokensFull
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
@@ -1449,6 +1508,10 @@ function handlers.textDocument_inlayHint(params)
|
||||
return hints
|
||||
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)
|
||||
local query = params.query or ""
|
||||
local max_results = params.maxResults or 50
|
||||
@@ -1586,6 +1649,10 @@ function handlers.workspace_symbol(params)
|
||||
return final_results
|
||||
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)
|
||||
local uri = params.textDocument.uri
|
||||
local doc = documents[uri]
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
-- SCL Parser utilities for the LSP server
|
||||
-- Extracts variables, types, and function definitions from SCL source code
|
||||
|
||||
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)
|
||||
local variables = {}
|
||||
local variable_positions = {}
|
||||
@@ -164,6 +170,9 @@ function M.extract_variables(content)
|
||||
return variables, variable_positions
|
||||
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)
|
||||
local types = {}
|
||||
|
||||
@@ -285,6 +294,9 @@ function M.extract_types(content)
|
||||
return types
|
||||
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)
|
||||
local functions = {}
|
||||
|
||||
|
||||
+33
-1
@@ -1,10 +1,14 @@
|
||||
-- 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 = {}
|
||||
|
||||
-- Cache for loaded types: { [root_dir] = { [type_name] = type_info } }
|
||||
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 handle = io.popen(cmd)
|
||||
if not handle then return nil end
|
||||
@@ -26,6 +30,9 @@ else
|
||||
script_path = "."
|
||||
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 json = dofile(script_path .. "/json.lua")
|
||||
local ok, data = pcall(function() return json.decode(content) end)
|
||||
@@ -33,6 +40,10 @@ local function load_json(content)
|
||||
return nil
|
||||
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 files = {}
|
||||
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
|
||||
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 type_name = dt.name or dt.Name or dt.baseType or dt.dataTypeName
|
||||
if not type_name then return nil end
|
||||
@@ -78,6 +92,9 @@ local function parse_plc_type(dt)
|
||||
return typ
|
||||
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 types = {}
|
||||
local in_type = false
|
||||
@@ -244,6 +261,9 @@ local function parse_scl_type_file(content)
|
||||
return types
|
||||
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 types = {}
|
||||
local in_var = false
|
||||
@@ -288,6 +308,10 @@ local function parse_db_file(content)
|
||||
return types
|
||||
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)
|
||||
local types = {}
|
||||
|
||||
@@ -375,6 +399,9 @@ function M.load_types_from_workspace(root_dir)
|
||||
return types
|
||||
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)
|
||||
if not root_dir or root_dir == "" then
|
||||
root_dir = "workspace"
|
||||
@@ -389,6 +416,10 @@ function M.get_types(root_dir)
|
||||
return types
|
||||
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)
|
||||
local json = dofile(script_path .. "/json.lua")
|
||||
|
||||
@@ -435,6 +466,7 @@ function M.generate_plc_json(root_dir, output_file)
|
||||
return false
|
||||
end
|
||||
|
||||
--- Clear all cached types
|
||||
function M.clear_cache()
|
||||
cached_types = {}
|
||||
end
|
||||
|
||||
+28
-1
@@ -1,10 +1,13 @@
|
||||
-- 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 ts = nil
|
||||
|
||||
--- Initialize tree-sitter parser
|
||||
--- Attempts to load vim.treesitter module
|
||||
--- @return boolean True if tree-sitter is available
|
||||
function M.init()
|
||||
local ok, treesitter = pcall(require, "vim.treesitter")
|
||||
if ok then
|
||||
@@ -14,6 +17,10 @@ function M.init()
|
||||
return false
|
||||
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)
|
||||
if not ts then
|
||||
return nil, "Tree-sitter not available"
|
||||
@@ -32,6 +39,11 @@ function M.parse_scl(content)
|
||||
return tree, nil
|
||||
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)
|
||||
local variables = {}
|
||||
local variable_positions = {}
|
||||
@@ -79,6 +91,10 @@ function M.extract_variables_ts(tree, bufnr)
|
||||
return variables, variable_positions
|
||||
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)
|
||||
local functions = {}
|
||||
|
||||
@@ -109,6 +125,11 @@ function M.extract_functions_ts(tree, bufnr)
|
||||
return functions
|
||||
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)
|
||||
if not tree then return nil end
|
||||
|
||||
@@ -124,6 +145,12 @@ function M.get_node_at_position(tree, row, col)
|
||||
return nil
|
||||
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)
|
||||
if not tree or not ts then return nil end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user