feat: improve formatter and attribute block handling
- Format multi-line assignments with proper alignment - Format FB calls in multiline style with parameter alignment - Auto-expand collapsed attribute blocks before save - Rename command from LspSCLFormat to SCLFormat - Condense AGENTS.md and add testing section with reference project
This commit is contained in:
+214
-218
@@ -5,11 +5,8 @@ local M = {}
|
||||
-- Default patterns for attribute blocks to collapse
|
||||
-- Users can extend these via options.collapse_patterns
|
||||
local DEFAULT_COLLAPSE_PATTERNS = {
|
||||
-- Match S7 variable attributes: {EXTERNALACCESSIBLE := 'false'; ...}
|
||||
"^%s*{%s*EXTERNAL",
|
||||
-- Match S7 block attributes: { S7_Optimized_Access := 'TRUE' } or {S7_...}
|
||||
"^%s*{%s*S7_",
|
||||
-- Match generic attribute blocks with assignments
|
||||
"^%s*{%s*%w+%s*:=",
|
||||
}
|
||||
|
||||
@@ -26,20 +23,17 @@ end
|
||||
|
||||
-- Block types with their corresponding enders
|
||||
local BLOCKS = {
|
||||
-- Control structures
|
||||
["IF"] = "END_IF",
|
||||
["FOR"] = "END_FOR",
|
||||
["WHILE"] = "END_WHILE",
|
||||
["REPEAT"] = "END_REPEAT",
|
||||
["CASE"] = "END_CASE",
|
||||
["REGION"] = "END_REGION",
|
||||
-- Declarations
|
||||
["FUNCTION"] = "END_FUNCTION",
|
||||
["FUNCTION_BLOCK"] = "END_FUNCTION_BLOCK",
|
||||
["ORGANIZATION_BLOCK"] = "END_ORGANIZATION_BLOCK",
|
||||
["TYPE"] = "END_TYPE",
|
||||
["STRUCT"] = "END_STRUCT",
|
||||
-- VAR sections (special handling)
|
||||
["VAR"] = "END_VAR",
|
||||
["VAR_INPUT"] = "END_VAR",
|
||||
["VAR_OUTPUT"] = "END_VAR",
|
||||
@@ -50,7 +44,6 @@ local BLOCKS = {
|
||||
["VAR_NON_RETAIN"] = "END_VAR",
|
||||
}
|
||||
|
||||
-- Keywords that don't change indent level themselves (handled specially)
|
||||
local SAME_LEVEL = {
|
||||
["THEN"] = true,
|
||||
["ELSE"] = true,
|
||||
@@ -60,7 +53,6 @@ local SAME_LEVEL = {
|
||||
["BEGIN"] = true,
|
||||
}
|
||||
|
||||
-- Control structure enders that need semicolons
|
||||
local NEEDS_SEMICOLON = {
|
||||
["END_IF"] = true,
|
||||
["END_FOR"] = true,
|
||||
@@ -70,7 +62,6 @@ local NEEDS_SEMICOLON = {
|
||||
["END_REGION"] = true,
|
||||
}
|
||||
|
||||
-- VAR keywords for special handling
|
||||
local VAR_KEYWORDS = {
|
||||
["VAR"] = true,
|
||||
["VAR_INPUT"] = true,
|
||||
@@ -82,7 +73,6 @@ local VAR_KEYWORDS = {
|
||||
["VAR_NON_RETAIN"] = true,
|
||||
}
|
||||
|
||||
-- Declaration keywords (reset indent)
|
||||
local DECLARATIONS = {
|
||||
["FUNCTION"] = true,
|
||||
["FUNCTION_BLOCK"] = true,
|
||||
@@ -91,31 +81,32 @@ local DECLARATIONS = {
|
||||
["STRUCT"] = true,
|
||||
}
|
||||
|
||||
local CONTINUATION_OPERATORS = {
|
||||
["AND"] = true,
|
||||
["OR"] = true,
|
||||
["XOR"] = true,
|
||||
["NOT"] = true,
|
||||
["MOD"] = true,
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
-- Attribute block collapsing options
|
||||
-- NOTE: Disabled by default to preserve compatibility with attr_toggle.
|
||||
-- When formatter collapses blocks, the original content is lost.
|
||||
-- Use :SCLCollapseAllAttrBlocks after formatting instead.
|
||||
local collapse_attributes = options.collapseAttributes
|
||||
if collapse_attributes == nil then
|
||||
collapse_attributes = false -- Default: disabled to preserve toggle functionality
|
||||
collapse_attributes = false
|
||||
end
|
||||
|
||||
-- User can provide custom patterns or extend defaults
|
||||
local collapse_patterns = options.collapsePatterns or DEFAULT_COLLAPSE_PATTERNS
|
||||
if options.extendCollapsePatterns then
|
||||
-- Extend defaults with user patterns
|
||||
for _, pattern in ipairs(options.extendCollapsePatterns) do
|
||||
table.insert(collapse_patterns, pattern)
|
||||
end
|
||||
end
|
||||
|
||||
-- Parse content into lines
|
||||
local lines = {}
|
||||
for line in content:gmatch("([^\n]*)\n") do
|
||||
table.insert(lines, line)
|
||||
@@ -128,18 +119,9 @@ function M.format_document(content, options)
|
||||
return { { range = range(0, 0, 0, 0), newText = "" } }
|
||||
end
|
||||
|
||||
-- Stack of active blocks
|
||||
-- Each entry: { type = "IF", indent = 1, is_case = false }
|
||||
local stack = {}
|
||||
local formatted_lines = {}
|
||||
|
||||
-- FB call state
|
||||
local in_fb_call = false
|
||||
local fb_name_length = 0
|
||||
local fb_call_first_line = false
|
||||
local fb_paren_offset = 0 -- Offset to align parameters with opening paren
|
||||
|
||||
-- Helper: get current indent string
|
||||
local function get_indent()
|
||||
local level = 0
|
||||
for _, block in ipairs(stack) do
|
||||
@@ -150,100 +132,26 @@ function M.format_document(content, options)
|
||||
return string.rep(indent_char, level)
|
||||
end
|
||||
|
||||
-- Helper: get continuation indent for FB calls
|
||||
local function get_continuation_indent()
|
||||
local base = get_indent()
|
||||
return base .. string.rep(" ", fb_paren_offset)
|
||||
local function is_assignment_continuation(trimmed)
|
||||
if not trimmed then return false end
|
||||
local first_word = trimmed:match("^(%w+)")
|
||||
if first_word and CONTINUATION_OPERATORS[first_word:upper()] then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Helper: check if line is a case label
|
||||
local function is_case_label(trimmed)
|
||||
return trimmed:match("^[%w_#]+%s*:%s*$") ~= nil
|
||||
end
|
||||
|
||||
-- Helper: extract variable name from a line (before any attribute block)
|
||||
local function extract_variable_name(line)
|
||||
-- Match pattern: variableName{...} or variableName : type
|
||||
-- Variable names can contain letters, numbers, underscores
|
||||
local var_name = line:match("^%s*([%w_]+)%s*[%({:]")
|
||||
return var_name
|
||||
end
|
||||
|
||||
-- Helper: collapse attribute blocks like {EXTERNALACCESSIBLE := 'false'; ...} to {...}
|
||||
local function collapse_attribute_block(line)
|
||||
if not collapse_attributes then
|
||||
return line
|
||||
end
|
||||
|
||||
-- Check if line contains an attribute block
|
||||
local attr_start, attr_end = line:find("{.-}")
|
||||
if not attr_start then
|
||||
return line
|
||||
end
|
||||
|
||||
-- Extract the content before the attribute block
|
||||
local before_attr = line:sub(1, attr_start - 1)
|
||||
local attr_content = line:sub(attr_start, attr_end)
|
||||
local after_attr = line:sub(attr_end + 1)
|
||||
|
||||
-- Extract variable name for per-variable rules
|
||||
local var_name = extract_variable_name(line)
|
||||
|
||||
-- Check per-variable collapse rules first (highest priority)
|
||||
if options.collapseVariableRules and var_name then
|
||||
for _, rule in ipairs(options.collapseVariableRules) do
|
||||
local var_pattern = rule.variablePattern
|
||||
local attr_pattern = rule.attributePattern
|
||||
local should_collapse = rule.collapse
|
||||
|
||||
-- Check if variable name matches
|
||||
local var_matches = not var_pattern or var_name:match(var_pattern)
|
||||
|
||||
-- Check if attribute content matches (if specified)
|
||||
local attr_matches = not attr_pattern or attr_content:match(attr_pattern)
|
||||
|
||||
if var_matches and attr_matches then
|
||||
if should_collapse then
|
||||
return before_attr .. "{...}" .. after_attr
|
||||
else
|
||||
-- Explicitly don't collapse this match
|
||||
return line
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if this attribute block matches any global collapse pattern
|
||||
for _, pattern in ipairs(collapse_patterns) do
|
||||
if attr_content:match(pattern) then
|
||||
-- Collapse to {...}
|
||||
return before_attr .. "{...}" .. after_attr
|
||||
end
|
||||
end
|
||||
|
||||
return line
|
||||
end
|
||||
|
||||
-- Helper: check if line starts FB call
|
||||
local function is_fb_call_start(line)
|
||||
local trimmed = line:gsub("^%s+", "")
|
||||
if not (trimmed:match("^#%w+") or trimmed:match('^"')) then
|
||||
local function is_fb_call_start(trimmed)
|
||||
if not (trimmed:match("^#?%w+") or trimmed:match('^"')) then
|
||||
return false
|
||||
end
|
||||
local paren_pos = trimmed:find("%(")
|
||||
if not paren_pos then return false end
|
||||
local assign_pos = trimmed:find(":=")
|
||||
if assign_pos and assign_pos < paren_pos then return false end
|
||||
local open_count, close_count = 0, 0
|
||||
for i = 1, #trimmed do
|
||||
local c = trimmed:sub(i, i)
|
||||
if c == "(" then open_count = open_count + 1
|
||||
elseif c == ")" then close_count = close_count + 1 end
|
||||
end
|
||||
return open_count > 0 and close_count < open_count
|
||||
return true
|
||||
end
|
||||
|
||||
-- Helper: check if FB call ends on this line
|
||||
local function fb_call_has_closing(line)
|
||||
local open_count, close_count = 0, 0
|
||||
for i = 1, #line do
|
||||
@@ -251,23 +159,129 @@ function M.format_document(content, options)
|
||||
if c == "(" then open_count = open_count + 1
|
||||
elseif c == ")" then close_count = close_count + 1 end
|
||||
end
|
||||
-- FB call ends when we have closes > opens, not when they're equal
|
||||
-- (equal means no parens on this line, which shouldn't end the call)
|
||||
return close_count > 0 and close_count >= open_count
|
||||
end
|
||||
|
||||
-- Helper: check if line ends with comma (FB call continuation)
|
||||
local function is_continuation(line)
|
||||
return line:gsub("^%s+", ""):gsub("%s+$", ""):match(".*,$") ~= nil
|
||||
local function format_fb_call_multiline(fb_lines, base_indent, param_indent)
|
||||
local result = {}
|
||||
local all_text = table.concat(fb_lines, " ")
|
||||
|
||||
local fb_name = all_text:match("^(#?[%w_]+)%s*%(")
|
||||
if not fb_name then
|
||||
for _, l in ipairs(fb_lines) do
|
||||
table.insert(result, base_indent .. l)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local paren_start = all_text:find("%(")
|
||||
local paren_end = all_text:find("%);%s*$") or all_text:find("%)%s*$")
|
||||
if not paren_start or not paren_end then
|
||||
for _, l in ipairs(fb_lines) do
|
||||
table.insert(result, base_indent .. l)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local params_text = all_text:sub(paren_start + 1, paren_end - 1)
|
||||
|
||||
local params = {}
|
||||
local current_param = ""
|
||||
local depth = 0
|
||||
for i = 1, #params_text do
|
||||
local c = params_text:sub(i, i)
|
||||
if c == "(" then
|
||||
depth = depth + 1
|
||||
current_param = current_param .. c
|
||||
elseif c == ")" then
|
||||
depth = depth - 1
|
||||
current_param = current_param .. c
|
||||
elseif c == "," and depth == 0 then
|
||||
local trimmed_param = current_param:match("^%s*(.-)%s*$")
|
||||
if trimmed_param ~= "" then
|
||||
table.insert(params, trimmed_param)
|
||||
end
|
||||
current_param = ""
|
||||
else
|
||||
current_param = current_param .. c
|
||||
end
|
||||
end
|
||||
local last_param = current_param:match("^%s*(.-)%s*$")
|
||||
if last_param and last_param ~= "" then
|
||||
table.insert(params, last_param)
|
||||
end
|
||||
|
||||
if #params == 0 then
|
||||
return { base_indent .. fb_name .. "();" }
|
||||
end
|
||||
|
||||
table.insert(result, base_indent .. fb_name .. "(" .. params[1] .. ",")
|
||||
|
||||
for i = 2, #params do
|
||||
local closing = (i == #params) and ");" or ","
|
||||
table.insert(result, param_indent .. params[i] .. closing)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function is_case_label(trimmed)
|
||||
return trimmed:match("^[%w_#]+%s*:%s*$") ~= nil
|
||||
end
|
||||
|
||||
local function extract_variable_name(line)
|
||||
local var_name = line:match("^%s*([%w_]+)%s*[%({:]")
|
||||
return var_name
|
||||
end
|
||||
|
||||
local function collapse_attribute_block(line)
|
||||
if not collapse_attributes then
|
||||
return line
|
||||
end
|
||||
|
||||
local attr_start, attr_end = line:find("{.-}")
|
||||
if not attr_start then
|
||||
return line
|
||||
end
|
||||
|
||||
local before_attr = line:sub(1, attr_start - 1)
|
||||
local attr_content = line:sub(attr_start, attr_end)
|
||||
local after_attr = line:sub(attr_end + 1)
|
||||
|
||||
local var_name = extract_variable_name(line)
|
||||
|
||||
if options.collapseVariableRules and var_name then
|
||||
for _, rule in ipairs(options.collapseVariableRules) do
|
||||
local var_pattern = rule.variablePattern
|
||||
local attr_pattern = rule.attributePattern
|
||||
local should_collapse = rule.collapse
|
||||
|
||||
local var_matches = not var_pattern or var_name:match(var_pattern)
|
||||
local attr_matches = not attr_pattern or attr_content:match(attr_pattern)
|
||||
|
||||
if var_matches and attr_matches then
|
||||
if should_collapse then
|
||||
return before_attr .. "{...}" .. after_attr
|
||||
else
|
||||
return line
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, pattern in ipairs(collapse_patterns) do
|
||||
if attr_content:match(pattern) then
|
||||
return before_attr .. "{...}" .. after_attr
|
||||
end
|
||||
end
|
||||
|
||||
return line
|
||||
end
|
||||
|
||||
-- Helper: extract keyword from line
|
||||
local function get_keyword(trimmed)
|
||||
-- Match VAR_IN_OUT, VAR_CONSTANT, etc. (words with multiple underscores)
|
||||
return trimmed:match("^([%w_]+)") or trimmed:match("^(%w+)")
|
||||
end
|
||||
|
||||
-- Helper: find case block in stack
|
||||
local function find_case_block()
|
||||
for i = #stack, 1, -1 do
|
||||
if stack[i].type == "CASE" then
|
||||
@@ -277,7 +291,6 @@ function M.format_document(content, options)
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
-- Helper: pop blocks until matching ender found
|
||||
local function pop_to_ender(ender_type)
|
||||
while #stack > 0 do
|
||||
local top = stack[#stack]
|
||||
@@ -290,77 +303,83 @@ function M.format_document(content, options)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Process each line
|
||||
-- State tracking
|
||||
local in_fb_call = false
|
||||
local fb_call_lines = {}
|
||||
local fb_base_indent = ""
|
||||
local fb_param_indent = ""
|
||||
|
||||
local in_assignment = false
|
||||
local assignment_continuation_indent = ""
|
||||
|
||||
for line_num, line in ipairs(lines) do
|
||||
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local kw = get_keyword(trimmed)
|
||||
|
||||
-- Handle empty lines (preserve as-is)
|
||||
if trimmed == "" then
|
||||
table.insert(formatted_lines, "")
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Handle comments (re-indent with current level)
|
||||
if trimmed:match("^//") or trimmed:match("^%(") then
|
||||
-- Apply attribute block collapsing to comments too (for consistency)
|
||||
local collapsed = collapse_attribute_block(trimmed)
|
||||
table.insert(formatted_lines, get_indent() .. collapsed)
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Apply attribute block collapsing
|
||||
trimmed = collapse_attribute_block(trimmed)
|
||||
|
||||
-- Check for FB call start
|
||||
if not in_fb_call and is_fb_call_start(trimmed) then
|
||||
local fb_name = trimmed:match("^#?([%w_]+)") or trimmed:match('^"?([%w_]+)"?')
|
||||
if fb_name then
|
||||
fb_name_length = #fb_name
|
||||
in_fb_call = true
|
||||
fb_call_first_line = true
|
||||
-- Calculate paren offset: find position of opening paren + 1
|
||||
-- This aligns continuation lines with the content inside the parens
|
||||
local paren_pos = trimmed:find("%(")
|
||||
if paren_pos then
|
||||
fb_paren_offset = paren_pos + 1 -- +1 to align with content after paren
|
||||
else
|
||||
fb_paren_offset = fb_name_length + 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle FB call lines
|
||||
if in_fb_call then
|
||||
local is_first = fb_call_first_line
|
||||
local has_closing = fb_call_has_closing(trimmed)
|
||||
local is_cont = is_continuation(trimmed)
|
||||
|
||||
local formatted
|
||||
if has_closing then
|
||||
formatted = (is_first and get_indent() or get_continuation_indent()) .. trimmed
|
||||
in_fb_call = false
|
||||
fb_call_first_line = false
|
||||
elseif is_cont then
|
||||
formatted = is_first and (get_indent() .. trimmed) or (get_continuation_indent() .. trimmed)
|
||||
fb_call_first_line = false
|
||||
else
|
||||
formatted = get_continuation_indent() .. trimmed
|
||||
end
|
||||
table.insert(formatted_lines, formatted)
|
||||
if trimmed == "" then
|
||||
table.insert(formatted_lines, "")
|
||||
goto continue
|
||||
end
|
||||
|
||||
if trimmed:match("^//") or trimmed:match("^%(") then
|
||||
local collapsed = collapse_attribute_block(trimmed)
|
||||
table.insert(formatted_lines, get_indent() .. collapsed)
|
||||
goto continue
|
||||
end
|
||||
|
||||
trimmed = collapse_attribute_block(trimmed)
|
||||
|
||||
-- Check if continuing assignment
|
||||
if in_assignment and is_assignment_continuation(trimmed) then
|
||||
table.insert(formatted_lines, assignment_continuation_indent .. trimmed)
|
||||
goto continue
|
||||
end
|
||||
in_assignment = false
|
||||
|
||||
-- Check if starting FB call
|
||||
if not in_fb_call and is_fb_call_start(trimmed) then
|
||||
in_fb_call = true
|
||||
fb_call_lines = { trimmed }
|
||||
fb_base_indent = get_indent()
|
||||
local fb_name = trimmed:match("^(#?[%w_]+)")
|
||||
if fb_name then
|
||||
fb_param_indent = fb_base_indent .. string.rep(" ", #fb_name + 1)
|
||||
else
|
||||
fb_param_indent = fb_base_indent .. string.rep(" ", 4)
|
||||
end
|
||||
|
||||
if fb_call_has_closing(trimmed) then
|
||||
local formatted_fb = format_fb_call_multiline(fb_call_lines, fb_base_indent, fb_param_indent)
|
||||
for _, fline in ipairs(formatted_fb) do
|
||||
table.insert(formatted_lines, fline)
|
||||
end
|
||||
in_fb_call = false
|
||||
fb_call_lines = {}
|
||||
end
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Continue collecting FB call lines
|
||||
if in_fb_call then
|
||||
table.insert(fb_call_lines, trimmed)
|
||||
if fb_call_has_closing(trimmed) then
|
||||
local formatted_fb = format_fb_call_multiline(fb_call_lines, fb_base_indent, fb_param_indent)
|
||||
for _, fline in ipairs(formatted_fb) do
|
||||
table.insert(formatted_lines, fline)
|
||||
end
|
||||
in_fb_call = false
|
||||
fb_call_lines = {}
|
||||
end
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Handle block declarations (FUNCTION_BLOCK, etc.)
|
||||
if DECLARATIONS[kw] then
|
||||
-- Clear stack for new block
|
||||
stack = {}
|
||||
table.insert(formatted_lines, trimmed)
|
||||
table.insert(stack, { type = kw, indent = 0, is_declaration = true })
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Handle declaration enders
|
||||
if kw and not BLOCKS[kw] then
|
||||
for starter, ender in pairs(BLOCKS) do
|
||||
if DECLARATIONS[starter] and kw == ender then
|
||||
@@ -371,20 +390,15 @@ function M.format_document(content, options)
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle BEGIN (transition to code section)
|
||||
if kw == "BEGIN" then
|
||||
-- Pop any VAR blocks
|
||||
while #stack > 0 and VAR_KEYWORDS[stack[#stack].type] do
|
||||
table.remove(stack)
|
||||
end
|
||||
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||
-- BEGIN itself doesn't add indent, but code inside does
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Handle VAR section start
|
||||
if VAR_KEYWORDS[kw] then
|
||||
-- Pop to declaration level
|
||||
while #stack > 0 and not stack[#stack].is_declaration do
|
||||
table.remove(stack)
|
||||
end
|
||||
@@ -393,9 +407,7 @@ function M.format_document(content, options)
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Handle END_VAR
|
||||
if kw == "END_VAR" then
|
||||
-- Pop VAR blocks
|
||||
while #stack > 0 and VAR_KEYWORDS[stack[#stack].type] do
|
||||
table.remove(stack)
|
||||
end
|
||||
@@ -403,14 +415,10 @@ function M.format_document(content, options)
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Handle block enders (END_IF, END_FOR, etc.)
|
||||
if kw and BLOCKS[kw] == nil then
|
||||
-- Check if it's an ender
|
||||
for starter, ender in pairs(BLOCKS) do
|
||||
if kw == ender and not DECLARATIONS[starter] then
|
||||
-- Pop matching block
|
||||
pop_to_ender(kw)
|
||||
-- Add semicolon if needed
|
||||
local formatted = get_indent() .. trimmed
|
||||
if NEEDS_SEMICOLON[kw] and trimmed:sub(-1) ~= ";" then
|
||||
formatted = formatted .. ";"
|
||||
@@ -421,11 +429,9 @@ function M.format_document(content, options)
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle same-level keywords (THEN, ELSE, ELSIF)
|
||||
if SAME_LEVEL[kw] then
|
||||
local formatted
|
||||
if kw == "THEN" then
|
||||
-- Pop IF block temporarily to get correct indent
|
||||
local if_block = nil
|
||||
for i = #stack, 1, -1 do
|
||||
if stack[i].type == "IF" then
|
||||
@@ -439,14 +445,11 @@ function M.format_document(content, options)
|
||||
formatted = get_indent() .. trimmed
|
||||
end
|
||||
table.insert(formatted_lines, formatted)
|
||||
-- Push content block after THEN
|
||||
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
||||
elseif kw == "ELSE" or kw == "ELSIF" then
|
||||
-- Pop THEN_CONTENT if present
|
||||
if #stack > 0 and stack[#stack].type == "THEN_CONTENT" then
|
||||
table.remove(stack)
|
||||
end
|
||||
-- Find IF block for correct indent
|
||||
local if_block = nil
|
||||
for i = #stack, 1, -1 do
|
||||
if stack[i].type == "IF" then
|
||||
@@ -460,7 +463,6 @@ function M.format_document(content, options)
|
||||
formatted = get_indent() .. trimmed
|
||||
end
|
||||
table.insert(formatted_lines, formatted)
|
||||
-- Push new content block
|
||||
table.insert(stack, { type = "THEN_CONTENT", indent = #stack, no_indent = true })
|
||||
else
|
||||
formatted = get_indent() .. trimmed
|
||||
@@ -469,18 +471,14 @@ function M.format_document(content, options)
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Handle case labels
|
||||
if is_case_label(trimmed) then
|
||||
local case_block, case_idx = find_case_block()
|
||||
if case_block then
|
||||
-- Pop to just after CASE
|
||||
while #stack > case_idx do
|
||||
table.remove(stack)
|
||||
end
|
||||
-- Case label at CASE level + 1
|
||||
local label_indent = string.rep(indent_char, case_block.indent + 1)
|
||||
table.insert(formatted_lines, label_indent .. trimmed)
|
||||
-- Push case content level
|
||||
table.insert(stack, { type = "CASE_LABEL", indent = case_block.indent + 1, no_indent = true })
|
||||
else
|
||||
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||
@@ -488,7 +486,6 @@ function M.format_document(content, options)
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Handle block starters (IF, FOR, CASE, REGION, etc.)
|
||||
if kw and BLOCKS[kw] and not VAR_KEYWORDS[kw] and not DECLARATIONS[kw] then
|
||||
local current_level = 0
|
||||
for _, block in ipairs(stack) do
|
||||
@@ -502,9 +499,24 @@ function M.format_document(content, options)
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Regular line (including variable declarations inside VAR sections)
|
||||
-- Regular line - check for assignment start
|
||||
if 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 not trimmed:match(";%s*$") then
|
||||
in_assignment = true
|
||||
end
|
||||
end
|
||||
else
|
||||
in_assignment = false
|
||||
end
|
||||
|
||||
local formatted = get_indent() .. trimmed
|
||||
-- Add semicolon if it looks like it needs one
|
||||
if NEEDS_SEMICOLON[kw] and trimmed:sub(-1) ~= ";" then
|
||||
formatted = formatted .. ";"
|
||||
end
|
||||
@@ -513,7 +525,6 @@ function M.format_document(content, options)
|
||||
::continue::
|
||||
end
|
||||
|
||||
-- Join lines
|
||||
local result = table.concat(formatted_lines, "\n")
|
||||
if content:sub(-1) == "\n" then
|
||||
result = result .. "\n"
|
||||
@@ -534,22 +545,7 @@ function M.get_formatting_options()
|
||||
trimTrailingWhitespace = true,
|
||||
insertFinalNewline = true,
|
||||
trimFinalNewlines = true,
|
||||
collapseAttributes = false, -- Disabled by default to preserve toggle functionality
|
||||
-- Set to true to enable automatic collapsing during formatting:
|
||||
-- collapseAttributes = true,
|
||||
-- collapsePatterns = { ... }, -- Override default collapse patterns
|
||||
-- extendCollapsePatterns = { ... }, -- Extend default patterns with custom ones
|
||||
-- collapseVariableRules = { -- Per-variable collapse rules (highest priority)
|
||||
-- {
|
||||
-- variablePattern = "^stat", -- Match variable names starting with "stat"
|
||||
-- attributePattern = "EXTERNAL", -- Optional: also match attribute content
|
||||
-- collapse = true, -- true to collapse, false to expand
|
||||
-- },
|
||||
-- {
|
||||
-- variablePattern = "^temp", -- Match variables starting with "temp"
|
||||
-- collapse = false, -- Never collapse these
|
||||
-- },
|
||||
-- },
|
||||
collapseAttributes = false,
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user