Rewrite formatter with stack-based approach for proper nested structure handling
This commit is contained in:
+328
-274
@@ -1,5 +1,5 @@
|
|||||||
-- SCL Formatter - Document formatting for the LSP server
|
-- SCL Formatter - Document formatting for the LSP server
|
||||||
-- Conforms to TIA Portal SCL formatting standards
|
-- Stack-based implementation for proper nested structure handling
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local function position(line, character)
|
local function position(line, character)
|
||||||
@@ -13,355 +13,409 @@ local function range(start_line, start_char, end_line, end_char)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Keywords that start a code block (increase indent AFTER)
|
-- Block types with their corresponding enders
|
||||||
local block_starters = {
|
local BLOCKS = {
|
||||||
IF = true, FOR = true, WHILE = true, REPEAT = true,
|
-- Control structures
|
||||||
CASE = true, REGION = true,
|
["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",
|
||||||
|
["VAR_IN_OUT"] = "END_VAR",
|
||||||
|
["VAR_TEMP"] = "END_VAR",
|
||||||
|
["VAR_CONSTANT"] = "END_VAR",
|
||||||
|
["VAR_RETAIN"] = "END_VAR",
|
||||||
|
["VAR_NON_RETAIN"] = "END_VAR",
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Keywords that stay on same level (don't change indent)
|
-- Keywords that don't change indent level themselves (handled specially)
|
||||||
local same_level_keywords = {
|
local SAME_LEVEL = {
|
||||||
THEN = true, ELSE = true, ELSIF = true, DO = true, OF = true,
|
["THEN"] = true,
|
||||||
|
["ELSE"] = true,
|
||||||
|
["ELSIF"] = true,
|
||||||
|
["DO"] = true,
|
||||||
|
["OF"] = true,
|
||||||
|
["BEGIN"] = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Keywords that end a code block (decrease indent BEFORE)
|
-- Control structure enders that need semicolons
|
||||||
local block_enders = {
|
local NEEDS_SEMICOLON = {
|
||||||
END_IF = true, END_FOR = true, END_WHILE = true, END_REPEAT = true,
|
["END_IF"] = true,
|
||||||
END_CASE = true, END_REGION = true,
|
["END_FOR"] = true,
|
||||||
|
["END_WHILE"] = true,
|
||||||
|
["END_REPEAT"] = true,
|
||||||
|
["END_CASE"] = true,
|
||||||
|
["END_REGION"] = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Block declaration keywords
|
-- VAR keywords for special handling
|
||||||
local block_declarations = {
|
local VAR_KEYWORDS = {
|
||||||
FUNCTION = true, FUNCTION_BLOCK = true, ORGANIZATION_BLOCK = true,
|
["VAR"] = true,
|
||||||
TYPE = true, STRUCT = true,
|
["VAR_INPUT"] = true,
|
||||||
|
["VAR_OUTPUT"] = true,
|
||||||
|
["VAR_IN_OUT"] = true,
|
||||||
|
["VAR_TEMP"] = true,
|
||||||
|
["VAR_CONSTANT"] = true,
|
||||||
|
["VAR_RETAIN"] = true,
|
||||||
|
["VAR_NON_RETAIN"] = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
-- End block declarations
|
-- Declaration keywords (reset indent)
|
||||||
local block_declaration_enders = {
|
local DECLARATIONS = {
|
||||||
END_FUNCTION = true, END_FUNCTION_BLOCK = true, END_ORGANIZATION_BLOCK = true,
|
["FUNCTION"] = true,
|
||||||
END_TYPE = true, END_STRUCT = true,
|
["FUNCTION_BLOCK"] = true,
|
||||||
}
|
["ORGANIZATION_BLOCK"] = true,
|
||||||
|
["TYPE"] = true,
|
||||||
-- VAR section keywords
|
["STRUCT"] = true,
|
||||||
local var_keywords = {
|
|
||||||
VAR = true, VAR_INPUT = true, VAR_OUTPUT = true, VAR_IN_OUT = true,
|
|
||||||
VAR_TEMP = true, VAR_CONSTANT = true, VAR_RETAIN = true, VAR_NON_RETAIN = true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function M.format_document(content, options)
|
function M.format_document(content, options)
|
||||||
options = options or {}
|
options = options or {}
|
||||||
local indent_char = options.insertSpaces and " " or "\t"
|
local use_spaces = options.insertSpaces or false
|
||||||
local indent_size = options.tabSize or 1
|
local indent_size = options.tabSize or 1
|
||||||
|
local indent_char = use_spaces and string.rep(" ", indent_size) or "\t"
|
||||||
|
|
||||||
|
-- Parse content into lines
|
||||||
local lines = {}
|
local lines = {}
|
||||||
for line in content:gmatch("([^\n]*)\n") do
|
for line in content:gmatch("([^\n]*)\n") do
|
||||||
table.insert(lines, line)
|
table.insert(lines, line)
|
||||||
end
|
end
|
||||||
if #lines == 0 or content:sub(-1) ~= "\n" then
|
local last_line = content:match("([^\n]+)$")
|
||||||
table.insert(lines, "")
|
if last_line and last_line ~= "" then
|
||||||
|
table.insert(lines, last_line)
|
||||||
|
end
|
||||||
|
if #lines == 0 then
|
||||||
|
return { { range = range(0, 0, 0, 0), newText = "" } }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Stack of active blocks
|
||||||
|
-- Each entry: { type = "IF", indent = 1, is_case = false }
|
||||||
|
local stack = {}
|
||||||
local formatted_lines = {}
|
local formatted_lines = {}
|
||||||
local indent_level = 0
|
|
||||||
local if_base_level = nil
|
|
||||||
local in_var_section = false
|
|
||||||
local in_code_section = false
|
|
||||||
local prev_line_keyword = nil
|
|
||||||
local in_multi_line_condition = false -- Track if we're in multi-line IF condition
|
|
||||||
|
|
||||||
-- FB call continuation tracking
|
-- FB call state
|
||||||
local in_fb_call = false
|
local in_fb_call = false
|
||||||
local fb_call_indent = 0
|
|
||||||
local fb_name_length = 0
|
local fb_name_length = 0
|
||||||
local fb_call_first_line = false
|
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 function get_indent()
|
||||||
return string.rep(indent_char, indent_level * indent_size)
|
local level = 0
|
||||||
end
|
for _, block in ipairs(stack) do
|
||||||
|
if not block.no_indent then
|
||||||
local function get_continuation_indent()
|
level = level + 1
|
||||||
-- Continuation indent to align parameters
|
end
|
||||||
-- Aligns parameter name with position after FB name + opening paren
|
|
||||||
-- Formula: base indent + FB name length + 1 (for opening paren) + 1 (extra offset)
|
|
||||||
local base_indent = string.rep(indent_char, indent_level * indent_size)
|
|
||||||
return base_indent .. string.rep(" ", fb_name_length + 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function get_keyword(line)
|
|
||||||
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
|
||||||
local kw = trimmed:match("^(%w+)")
|
|
||||||
return kw, trimmed
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_block_starter(kw)
|
|
||||||
return kw and block_starters[kw]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_block_ender(kw)
|
|
||||||
return kw and block_enders[kw]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_var_keyword(kw)
|
|
||||||
return kw and var_keywords[kw]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_block_declaration(kw)
|
|
||||||
return kw and block_declarations[kw]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_block_declaration_ender(kw)
|
|
||||||
return kw and block_declaration_enders[kw]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function needs_semicolon(trimmed)
|
|
||||||
local kw = trimmed:match("^(%w+_%w+)") or trimmed:match("^(%w+)")
|
|
||||||
if not kw then return false end
|
|
||||||
local needs_end = {
|
|
||||||
END_IF = true, END_FOR = true, END_WHILE = true, END_REPEAT = true,
|
|
||||||
END_CASE = true, END_REGION = true,
|
|
||||||
END_FUNCTION = true, END_FUNCTION_BLOCK = true, END_ORGANIZATION_BLOCK = true,
|
|
||||||
END_TYPE = true, END_STRUCT = true,
|
|
||||||
}
|
|
||||||
if needs_end[kw] then
|
|
||||||
local last_char = trimmed:sub(-1)
|
|
||||||
return last_char ~= ";" and last_char ~= "("
|
|
||||||
end
|
end
|
||||||
return false
|
return string.rep(indent_char, level)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Helper: get continuation indent for FB calls
|
||||||
|
local function get_continuation_indent()
|
||||||
|
local base = get_indent()
|
||||||
|
return base .. string.rep(" ", fb_paren_offset)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: check if line is a case label
|
||||||
local function is_case_label(trimmed)
|
local function is_case_label(trimmed)
|
||||||
return trimmed:match("^%w+%s*:") ~= nil
|
return trimmed:match("^[%w_#]+%s*:%s*$") ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check if line is an FB/function call start
|
-- Helper: check if line starts FB call
|
||||||
-- Check if line is an FB/function call start
|
|
||||||
-- Matches: #fbname( or "FBName"( - but NOT if it has := before the (
|
|
||||||
local function is_fb_call_start(line)
|
local function is_fb_call_start(line)
|
||||||
local trimmed = line:gsub("^%s+", "")
|
local trimmed = line:gsub("^%s+", "")
|
||||||
-- Must start with # or "
|
if not (trimmed:match("^#%w+") or trimmed:match('^"')) then
|
||||||
local has_fb_syntax = trimmed:match("^#%w+") or trimmed:match('^"')
|
return false
|
||||||
if not has_fb_syntax then return false end
|
|
||||||
|
|
||||||
-- Check if there's a ( in the line
|
|
||||||
local paren_pos = trimmed:find("%(")
|
|
||||||
if not paren_pos then return false end -- No parentheses, not an FB call
|
|
||||||
|
|
||||||
-- Check if := appears BEFORE the ( (which would make it an assignment, not an FB call)
|
|
||||||
local assign_pos = trimmed:find(":=")
|
|
||||||
if assign_pos and assign_pos < paren_pos then
|
|
||||||
return false -- Has := before (, it's an assignment
|
|
||||||
end
|
end
|
||||||
|
local paren_pos = trimmed:find("%(")
|
||||||
-- Check if it has unclosed parentheses
|
if not paren_pos then return false end
|
||||||
local open_count = 0
|
local assign_pos = trimmed:find(":=")
|
||||||
local close_count = 0
|
if assign_pos and assign_pos < paren_pos then return false end
|
||||||
|
local open_count, close_count = 0, 0
|
||||||
for i = 1, #trimmed do
|
for i = 1, #trimmed do
|
||||||
local c = trimmed:sub(i, i)
|
local c = trimmed:sub(i, i)
|
||||||
if c == "(" then open_count = open_count + 1
|
if c == "(" then open_count = open_count + 1
|
||||||
elseif c == ")" then close_count = close_count + 1
|
elseif c == ")" then close_count = close_count + 1 end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
return open_count > 0 and close_count < open_count
|
return open_count > 0 and close_count < open_count
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check if line is inside an FB call (has unclosed parentheses)
|
-- Helper: check if FB call ends on this line
|
||||||
local function is_fb_call_inside(line)
|
local function fb_call_has_closing(line)
|
||||||
local open_count = 0
|
local open_count, close_count = 0, 0
|
||||||
local close_count = 0
|
|
||||||
for i = 1, #line do
|
for i = 1, #line do
|
||||||
local c = line:sub(i, i)
|
local c = line:sub(i, i)
|
||||||
if c == "(" then open_count = open_count + 1
|
if c == "(" then open_count = open_count + 1
|
||||||
elseif c == ")" then close_count = close_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
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: extract keyword from line
|
||||||
|
local function get_keyword(trimmed)
|
||||||
|
return trimmed:match("^(%w+_%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
|
||||||
|
return stack[i], i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return open_count > close_count
|
return nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check if line ends an FB call
|
-- Helper: pop blocks until matching ender found
|
||||||
local function is_fb_call_end(line)
|
local function pop_to_ender(ender_type)
|
||||||
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
while #stack > 0 do
|
||||||
return trimmed:match(".*%)%s*;?%s*$") ~= nil and
|
local top = stack[#stack]
|
||||||
not trimmed:match(".*%([^)]*%:?[=]") -- not still inside parameters
|
table.remove(stack)
|
||||||
|
local expected_ender = BLOCKS[top.type]
|
||||||
|
if expected_ender == ender_type then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check if line is a continuation (ends with ,)
|
-- Process each line
|
||||||
local function is_fb_call_continuation(line)
|
for line_num, line in ipairs(lines) do
|
||||||
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
return trimmed:match(".*[,]%s*$") ~= nil
|
local kw = get_keyword(trimmed)
|
||||||
end
|
|
||||||
|
|
||||||
for i, line in ipairs(lines) do
|
-- Handle empty lines (preserve as-is)
|
||||||
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
if trimmed == "" then
|
||||||
local original_indent = line:match("^%s*")
|
table.insert(formatted_lines, "")
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
-- Extract full keyword (handle compound keywords like END_IF, END_FUNCTION_BLOCK)
|
-- Handle comments (re-indent with current level)
|
||||||
local kw = trimmed:match("^(%w+_%w+)") or trimmed:match("^(%w+)")
|
if trimmed:match("^//") or trimmed:match("^%(") then
|
||||||
|
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
-- Check for FB call start (before comment handling)
|
-- Check for FB call start
|
||||||
-- Only detect if we're not already in an FB call
|
|
||||||
if not in_fb_call and is_fb_call_start(trimmed) then
|
if not in_fb_call and is_fb_call_start(trimmed) then
|
||||||
-- Extract FB name length (without quotes)
|
|
||||||
local fb_name = trimmed:match("^#?([%w_]+)") or trimmed:match('^"?([%w_]+)"?')
|
local fb_name = trimmed:match("^#?([%w_]+)") or trimmed:match('^"?([%w_]+)"?')
|
||||||
if fb_name then
|
if fb_name then
|
||||||
fb_name_length = #fb_name
|
fb_name_length = #fb_name
|
||||||
in_fb_call = true
|
in_fb_call = true
|
||||||
fb_call_indent = indent_level
|
|
||||||
fb_call_first_line = 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
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle comments - re-indent them
|
-- Handle FB call lines
|
||||||
if trimmed:match("^//") then
|
if in_fb_call then
|
||||||
-- Line comment
|
local is_first = fb_call_first_line
|
||||||
table.insert(formatted_lines, get_indent() .. trimmed)
|
local has_closing = fb_call_has_closing(trimmed)
|
||||||
elseif trimmed:match("^%(%*") or trimmed:match("^%*%/") then
|
local is_cont = is_continuation(trimmed)
|
||||||
-- Block comment at start of line
|
|
||||||
table.insert(formatted_lines, get_indent() .. trimmed)
|
|
||||||
elseif trimmed == "" then
|
|
||||||
-- Preserve empty lines as-is
|
|
||||||
table.insert(formatted_lines, line)
|
|
||||||
else
|
|
||||||
-- Handle FB call continuation
|
|
||||||
local formatted_line
|
|
||||||
if in_fb_call then
|
|
||||||
-- Check if this is the first line of the FB call
|
|
||||||
local is_first_line = fb_call_first_line
|
|
||||||
|
|
||||||
-- Check if line ends an FB call (has closing paren)
|
local formatted
|
||||||
local has_closing = trimmed:match(".*%)")
|
if has_closing then
|
||||||
if has_closing and not trimmed:match("%(") then
|
formatted = (is_first and get_indent() or get_continuation_indent()) .. trimmed
|
||||||
-- This line closes the FB call - use normal indent
|
in_fb_call = false
|
||||||
formatted_line = get_indent() .. trimmed
|
fb_call_first_line = false
|
||||||
in_fb_call = false
|
elseif is_cont then
|
||||||
fb_call_first_line = false
|
formatted = is_first and (get_indent() .. trimmed) or (get_continuation_indent() .. trimmed)
|
||||||
-- Check if this is a continuation line (ends with ,)
|
fb_call_first_line = false
|
||||||
elseif is_fb_call_continuation(trimmed) then
|
|
||||||
if is_first_line then
|
|
||||||
-- First line of FB call - use normal indent
|
|
||||||
formatted_line = get_indent() .. trimmed
|
|
||||||
fb_call_first_line = false
|
|
||||||
else
|
|
||||||
-- Continuation line - use continuation indent
|
|
||||||
formatted_line = get_continuation_indent() .. trimmed
|
|
||||||
end
|
|
||||||
else
|
|
||||||
-- Still inside FB call but not a continuation - use continuation indent
|
|
||||||
formatted_line = get_continuation_indent() .. trimmed
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
formatted_line = get_indent() .. trimmed
|
formatted = get_continuation_indent() .. trimmed
|
||||||
end
|
end
|
||||||
|
table.insert(formatted_lines, formatted)
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
-- Check for BEGIN keyword (starts code section)
|
-- Handle block declarations (FUNCTION_BLOCK, etc.)
|
||||||
if kw == "BEGIN" then
|
if DECLARATIONS[kw] then
|
||||||
in_code_section = true
|
-- Clear stack for new block
|
||||||
table.insert(formatted_lines, formatted_line)
|
stack = {}
|
||||||
indent_level = 1
|
table.insert(formatted_lines, trimmed)
|
||||||
-- Check for VAR section start
|
table.insert(stack, { type = kw, indent = 0, is_declaration = true })
|
||||||
elseif is_var_keyword(kw) then
|
goto continue
|
||||||
in_var_section = true
|
end
|
||||||
in_code_section = false
|
|
||||||
indent_level = 0
|
|
||||||
table.insert(formatted_lines, formatted_line)
|
|
||||||
indent_level = 1
|
|
||||||
-- Check for END_VAR
|
|
||||||
elseif kw == "END_VAR" then
|
|
||||||
in_var_section = false
|
|
||||||
indent_level = 0
|
|
||||||
table.insert(formatted_lines, formatted_line)
|
|
||||||
-- Check for block declaration enders
|
|
||||||
elseif is_block_declaration_ender(kw) then
|
|
||||||
indent_level = math.max(0, indent_level - 1)
|
|
||||||
if needs_semicolon(trimmed) then
|
|
||||||
formatted_line = formatted_line .. ";"
|
|
||||||
end
|
|
||||||
table.insert(formatted_lines, formatted_line)
|
|
||||||
-- Check for block declaration starters (FUNCTION, FUNCTION_BLOCK, etc.)
|
|
||||||
elseif is_block_declaration(kw) then
|
|
||||||
table.insert(formatted_lines, formatted_line)
|
|
||||||
indent_level = 0
|
|
||||||
-- Check for block enders
|
|
||||||
elseif is_block_ender(kw) then
|
|
||||||
if kw == "END_IF" then
|
|
||||||
if_base_level = nil
|
|
||||||
end
|
|
||||||
in_fb_call = false -- Reset FB call state
|
|
||||||
indent_level = math.max(0, indent_level - 1)
|
|
||||||
if needs_semicolon(trimmed) then
|
|
||||||
formatted_line = formatted_line .. ";"
|
|
||||||
end
|
|
||||||
table.insert(formatted_lines, formatted_line)
|
|
||||||
-- Check for CASE labels (like "1:")
|
|
||||||
elseif is_case_label(trimmed) and prev_line_keyword == "CASE" then
|
|
||||||
table.insert(formatted_lines, formatted_line)
|
|
||||||
indent_level = indent_level + 1
|
|
||||||
-- Check for same-level keywords (THEN, ELSE, DO, OF on their own line)
|
|
||||||
elseif same_level_keywords[kw] then
|
|
||||||
-- For THEN on its own line (after IF without THEN on same line), increase indent for content
|
|
||||||
if kw == "THEN" then
|
|
||||||
-- Increase indent after THEN for content
|
|
||||||
indent_level = indent_level + 1
|
|
||||||
end
|
|
||||||
-- For ELSIF and ELSE, reset to IF base level
|
|
||||||
if (kw == "ELSIF" or kw == "ELSE") and if_base_level then
|
|
||||||
indent_level = if_base_level
|
|
||||||
end
|
|
||||||
table.insert(formatted_lines, formatted_line)
|
|
||||||
-- After ELSE/ELSIF, increase indent for content
|
|
||||||
if (kw == "ELSIF" or kw == "ELSE") and if_base_level then
|
|
||||||
indent_level = indent_level + 1
|
|
||||||
end
|
|
||||||
-- Check for block starters
|
|
||||||
elseif is_block_starter(kw) then
|
|
||||||
-- Save base level for IF statements AFTER checking for THEN
|
|
||||||
if kw == "IF" then
|
|
||||||
-- Check if THEN is on the same line
|
|
||||||
if trimmed:match("THEN%s*$") then
|
|
||||||
if_base_level = indent_level
|
|
||||||
in_multi_line_condition = false
|
|
||||||
indent_level = indent_level + 1
|
|
||||||
else
|
|
||||||
-- THEN on separate line, don't increase yet
|
|
||||||
if_base_level = indent_level
|
|
||||||
in_multi_line_condition = true
|
|
||||||
end
|
|
||||||
elseif kw == "THEN" then
|
|
||||||
-- THEN on its own line, increase for content and exit multi-line condition
|
|
||||||
in_multi_line_condition = false
|
|
||||||
indent_level = indent_level + 1
|
|
||||||
else
|
|
||||||
-- For other block starters (FOR, WHILE, REGION, CASE), always increase
|
|
||||||
in_multi_line_condition = false
|
|
||||||
indent_level = indent_level + 1
|
|
||||||
end
|
|
||||||
table.insert(formatted_lines, formatted_line)
|
|
||||||
-- Regular line
|
|
||||||
else
|
|
||||||
-- Handle multi-line condition continuation
|
|
||||||
local use_multi_line_indent = false
|
|
||||||
if in_multi_line_condition and kw ~= "THEN" then
|
|
||||||
-- This is a continuation of a multi-line IF condition
|
|
||||||
-- Use +1 indent from IF level
|
|
||||||
use_multi_line_indent = true
|
|
||||||
in_multi_line_condition = false -- Exit multi-line mode after first continuation
|
|
||||||
end
|
|
||||||
|
|
||||||
if use_multi_line_indent then
|
-- Handle declaration enders
|
||||||
formatted_line = string.rep(indent_char, (indent_level + 1) * indent_size) .. trimmed
|
if kw and not BLOCKS[kw] then
|
||||||
else
|
for starter, ender in pairs(BLOCKS) do
|
||||||
formatted_line = get_indent() .. trimmed
|
if DECLARATIONS[starter] and kw == ender then
|
||||||
|
stack = {}
|
||||||
|
table.insert(formatted_lines, trimmed)
|
||||||
|
goto continue
|
||||||
end
|
end
|
||||||
|
|
||||||
if needs_semicolon(trimmed) and not in_fb_call then
|
|
||||||
formatted_line = formatted_line .. ";"
|
|
||||||
end
|
|
||||||
table.insert(formatted_lines, formatted_line)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
prev_line_keyword = kw
|
-- 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
|
||||||
|
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||||
|
table.insert(stack, { type = kw, indent = #stack, is_var = true })
|
||||||
|
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
|
||||||
|
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||||
|
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 .. ";"
|
||||||
|
end
|
||||||
|
table.insert(formatted_lines, formatted)
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
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
|
||||||
|
if_block = stack[i]
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if if_block then
|
||||||
|
formatted = string.rep(indent_char, if_block.indent) .. trimmed
|
||||||
|
else
|
||||||
|
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
|
||||||
|
if_block = stack[i]
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if if_block then
|
||||||
|
formatted = string.rep(indent_char, if_block.indent) .. trimmed
|
||||||
|
else
|
||||||
|
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
|
||||||
|
table.insert(formatted_lines, formatted)
|
||||||
|
end
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
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
|
||||||
|
if not block.no_indent then
|
||||||
|
current_level = current_level + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local formatted = string.rep(indent_char, current_level) .. trimmed
|
||||||
|
table.insert(formatted_lines, formatted)
|
||||||
|
table.insert(stack, { type = kw, indent = current_level })
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Regular line
|
||||||
|
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
|
||||||
|
table.insert(formatted_lines, formatted)
|
||||||
|
|
||||||
|
::continue::
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Join lines
|
||||||
local result = table.concat(formatted_lines, "\n")
|
local result = table.concat(formatted_lines, "\n")
|
||||||
if content:sub(-1) == "\n" then
|
if content:sub(-1) == "\n" then
|
||||||
result = result .. "\n"
|
result = result .. "\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user