feat: add interactive attribute block toggle with keybindings and commands
Add ability to expand/collapse SCL variable attribute blocks like {EXTERNALACCESSIBLE := 'false'} to {...}
Features:
- Per-variable collapse rules via collapseVariableRules option
- Interactive toggle with <Leader>xa keybinding
- Commands: SCLToggleAttrBlock, SCLExpandAllAttrBlocks, SCLCollapseAllAttrBlocks
- Keybindings: <Leader>xa (toggle), <Leader>xae (expand all), <Leader>xac (collapse all)
- Supports both formatter-collapsed and manually collapsed blocks
Also update AGENTS.md with new documentation and troubleshooting guide
This commit is contained in:
+26
-10
@@ -98,8 +98,8 @@ function M.validate_diagnostics(content, workspace_types, root_dir)
|
||||
end
|
||||
end
|
||||
|
||||
if in_var_section and trimmed:match("^[%w_]+%s*:") then
|
||||
local var_name = trimmed:match("^([%w_]+)%s*:")
|
||||
if in_var_section and (trimmed:match("^[%w_]+%s*%b{}%s*:") or trimmed:match("^[%w_]+%s*:")) then
|
||||
local var_name = trimmed:match("^([%w_]+)%s*%b{}%s*:") or trimmed:match("^([%w_]+)%s*:")
|
||||
local var_start = line:find(var_name, 1, true)
|
||||
|
||||
if var_name and not var_name:match("^%d") then
|
||||
@@ -114,21 +114,37 @@ function M.validate_diagnostics(content, workspace_types, root_dir)
|
||||
})
|
||||
end
|
||||
|
||||
local type_part = line:match(":%s*([^;]+)")
|
||||
local type_part = nil
|
||||
local brace_depth = 0
|
||||
local last_colon = nil
|
||||
for i = 1, #line do
|
||||
local c = line:sub(i, i)
|
||||
if c == "{" then
|
||||
brace_depth = brace_depth + 1
|
||||
elseif c == "}" then
|
||||
brace_depth = brace_depth - 1
|
||||
elseif c == ":" and brace_depth == 0 then
|
||||
last_colon = i
|
||||
end
|
||||
end
|
||||
if last_colon then
|
||||
type_part = line:sub(last_colon + 1)
|
||||
end
|
||||
if type_part then
|
||||
type_part = type_part:gsub("%s*:=.*$", "")
|
||||
type_part = type_part:gsub("%s*$", "")
|
||||
type_part = type_part:gsub("^%s+", "")
|
||||
|
||||
local base_type = type_part:match("^(%w+)")
|
||||
local upper_type = base_type and base_type:upper() or nil
|
||||
if base_type and not all_types[base_type] and
|
||||
base_type ~= "BOOL" and base_type ~= "INT" and base_type ~= "DINT" and
|
||||
base_type ~= "REAL" and base_type ~= "LREAL" and base_type ~= "BYTE" and
|
||||
base_type ~= "WORD" and base_type ~= "DWORD" and base_type ~= "LWORD" and
|
||||
base_type ~= "CHAR" and base_type ~= "STRING" and base_type ~= "WCHAR" and
|
||||
base_type ~= "WSTRING" and base_type ~= "TIME" and base_type ~= "DATE" and
|
||||
base_type ~= "TIME_OF_DAY" and base_type ~= "DATE_AND_TIME" and
|
||||
base_type ~= "S5TIME" and base_type ~= "LTIME" then
|
||||
upper_type ~= "BOOL" and upper_type ~= "INT" and upper_type ~= "DINT" and
|
||||
upper_type ~= "REAL" and upper_type ~= "LREAL" and upper_type ~= "BYTE" and
|
||||
upper_type ~= "WORD" and upper_type ~= "DWORD" and upper_type ~= "LWORD" and
|
||||
upper_type ~= "CHAR" and upper_type ~= "STRING" and upper_type ~= "WCHAR" and
|
||||
upper_type ~= "WSTRING" and upper_type ~= "TIME" and upper_type ~= "DATE" and
|
||||
upper_type ~= "TIME_OF_DAY" and upper_type ~= "DATE_AND_TIME" and
|
||||
upper_type ~= "S5TIME" and upper_type ~= "LTIME" then
|
||||
table.insert(diagnostics, {
|
||||
range = range_to_lsp(line_num, 0, line_num, #line),
|
||||
severity = DiagnosticSeverity.Warning,
|
||||
|
||||
+122
-14
@@ -2,6 +2,17 @@
|
||||
-- Stack-based implementation for proper nested structure handling
|
||||
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*:=",
|
||||
}
|
||||
|
||||
local function position(line, character)
|
||||
return { line = line, character = character }
|
||||
end
|
||||
@@ -85,6 +96,21 @@ function M.format_document(content, options)
|
||||
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
|
||||
local collapse_attributes = options.collapseAttributes
|
||||
if collapse_attributes == nil then
|
||||
collapse_attributes = true -- Default: enabled
|
||||
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 = {}
|
||||
@@ -127,10 +153,73 @@ function M.format_document(content, options)
|
||||
return base .. string.rep(" ", fb_paren_offset)
|
||||
end
|
||||
|
||||
-- Helper: check if line is a case label
|
||||
local function is_case_label(trimmed)
|
||||
return trimmed:match("^[%w_#]+%s*:%s*$") ~= nil
|
||||
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)
|
||||
@@ -202,17 +291,22 @@ function M.format_document(content, options)
|
||||
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 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
|
||||
table.insert(formatted_lines, get_indent() .. trimmed)
|
||||
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
|
||||
@@ -436,6 +530,20 @@ function M.get_formatting_options()
|
||||
trimTrailingWhitespace = true,
|
||||
insertFinalNewline = true,
|
||||
trimFinalNewlines = true,
|
||||
collapseAttributes = true, -- Collapse variable attribute blocks to {...}
|
||||
-- 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
|
||||
-- },
|
||||
-- },
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
+14
-3
@@ -78,15 +78,26 @@ function M.extract_variables(content)
|
||||
return
|
||||
end
|
||||
|
||||
local var_name = trimmed:match("^([%w_]+)%s*:")
|
||||
local var_name = trimmed:match("^([%w_]+)%s*%b{}%s*:") or trimmed:match("^([%w_]+)%s*:")
|
||||
if not var_name then
|
||||
var_name = trimmed:match("^([%w_]+)%s*,")
|
||||
var_name = trimmed:match("^([%w_]+)%s*%b{}%s*,") or trimmed:match("^([%w_]+)%s*,")
|
||||
end
|
||||
|
||||
if var_name and not variables[var_name] then
|
||||
local col = line:find(var_name, 1, true)
|
||||
if col then
|
||||
local type_start = line:find(":", 1, true)
|
||||
local type_start = nil
|
||||
local brace_depth = 0
|
||||
for i = 1, #line do
|
||||
local c = line:sub(i, i)
|
||||
if c == "{" then
|
||||
brace_depth = brace_depth + 1
|
||||
elseif c == "}" then
|
||||
brace_depth = brace_depth - 1
|
||||
elseif c == ":" and brace_depth == 0 then
|
||||
type_start = i
|
||||
end
|
||||
end
|
||||
local var_data_type = "unknown"
|
||||
if type_start then
|
||||
local type_part = line:sub(type_start + 1)
|
||||
|
||||
Reference in New Issue
Block a user