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:
2026-02-23 17:40:44 +01:00
parent db69592cc1
commit 37f8d475f0
9 changed files with 301 additions and 18 deletions
+56 -15
View File
@@ -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,19 +503,20 @@ function M.format_document(content, options)
if SAME_LEVEL[kw] then
local formatted
if kw == "THEN" then
local if_block = nil
for i = #stack, 1, -1 do
if stack[i].type == "IF" then
if_block = stack[i]
break
end
end
if if_block then
formatted = string.rep(indent_char, if_block.indent) .. trimmed
local then_indent
if in_condition and condition_indent ~= "" then
then_indent = condition_indent
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
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
in_condition = true
condition_indent = string.rep(indent_char, current_level)
-- 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
-- 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,