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:
2026-02-19 10:12:58 +01:00
parent 37ab917422
commit 978e46ba45
4 changed files with 377 additions and 125 deletions
+215 -98
View File
@@ -1,59 +1,63 @@
# AGENTS.md - SCL Language Server (Unified) # AGENTS.md - SCL Language Server
A unified Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion. A unified Neovim/LazyVim plugin for Siemens SCL language support providing LSP, linting, formatting, syntax highlighting, and auto-completion.
## Reference Project for Testing
For manual testing and SCL/UDT/DB file examples, use:
```
~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project
```
## Build/Lint/Test Commands ## Build/Lint/Test Commands
### LSP Server
```bash ```bash
# LSP Server
make start # Start LSP server make start # Start LSP server
make test # Run LSP in test mode make test # Run LSP in test mode
lua src/main.lua --test # Direct test mode execution lua src/main.lua --test # Direct test execution
```
### Tree-sitter Parser # Tree-sitter Parser
```bash
tree-sitter generate # Generate parser from grammar.js tree-sitter generate # Generate parser from grammar.js
tree-sitter test # Run tree-sitter tests tree-sitter test # Run parser tests
tree-sitter parse <file.scl> # Parse a single SCL file tree-sitter parse <file.scl> # Parse single SCL file
# Lua Linting
luacheck src/ # Lint LSP server code
luacheck lua/scl/ # Lint plugin modules
``` ```
## Project Structure ## Project Structure
``` ```
scl_lsp/ scl_lsp/
├── src/ # Standalone LSP server (uses dofile) ├── src/ # LSP server (uses dofile)
│ ├── main.lua # Entry point, JSON-RPC protocol │ ├── main.lua # Entry point, JSON-RPC protocol
│ ├── parser.lua # Regex-based SCL parser │ ├── parser.lua # SCL parser
│ ├── diagnostics.lua # Linter diagnostics │ ├── diagnostics.lua # Linter
── formatter.lua # Document formatter ── formatter.lua # Document formatter
├── lua/scl/ # Neovim plugin modules (uses require) │ ├── treesitter.lua # Tree-sitter integration
│ ├── blink_cmp_source.lua # blink.cmp completion source │ ├── plc_json.lua # External UDT loading
── udt_parser.lua # UDT (.udt) parser ── json.lua # JSON encoder/decoder
│ ├── db_parser.lua # Global DB (.db) parser ├── lua/scl/ # Neovim plugin (uses require)
│ ├── fb_parser.lua # Function Block parser │ ├── init.lua # Main plugin setup
│ ├── variables.lua # Local variable extraction │ ├── blink_cmp_source.lua
│ ├── workspace_types.lua # Workspace scanning │ ├── udt_parser.lua # UDT parser
── multiline_params.lua # FB parameter filling ── db_parser.lua # Global DB parser
└── queries/ # Tree-sitter queries │ ├── fb_parser.lua # Function Block parser
│ ├── variables.lua # Variable extraction
│ ├── workspace_types.lua # Workspace scanning
│ ├── multiline_params.lua
│ ├── auto_prefix.lua
│ └── attr_toggle.lua # Interactive attribute block toggle
├── queries/ # Tree-sitter queries
│ ├── highlights.scm
│ ├── indents.scm
│ └── folds.scm
└── grammar.js # Tree-sitter grammar
``` ```
## Code Style Guidelines ## Code Style Guidelines
### Module Pattern ### Module Pattern
All modules follow the standard Lua module pattern:
```lua ```lua
-- Module description comment at top -- File header comment
local M = {} local M = {}
-- Private module-level cache -- Private cache
local cache = {} local cache = {}
function M.public_function() function M.public_function()
@@ -66,37 +70,37 @@ return M
``` ```
### Naming Conventions ### Naming Conventions
- Functions/variables: `snake_case` (e.g., `get_udt_members`, `var_types`) - Functions/variables: `snake_case` (e.g., `get_udt_members`)
- Constants: `UPPER_SNAKE_CASE` (e.g., `PROJECT_MARKERS`) - Constants: `UPPER_SNAKE_CASE` (e.g., `PROJECT_MARKERS`)
- Private functions: declare as `local function` before public functions - Private functions: declare as `local function` before public functions
- Boolean variables: prefix with `is_`, `has_` (e.g., `is_udt`, `has_members`) - Booleans: prefix with `is_`, `has_` (e.g., `is_udt`, `has_members`)
### Imports ### Imports
- `lua/scl/` modules: use `require("scl.module_name")` - `lua/scl/` modules: use `require("scl.module_name")`
- `src/` modules: use `dofile(script_path .. "/module.lua")` - `src/` modules: use `dofile(script_path .. "/module.lua")`
- External dependencies: wrap in `pcall()` for safety - External dependencies: wrap in `pcall()` for safety
### Comments ### Code Formatting
- Every file must have a header comment explaining its purpose - `lua/scl/`: 2 spaces indentation
- All functions should have a brief comment describing what they do - `src/`: tab-based indentation
- Complex logic requires inline comments explaining the "why" - Max line length: 120 characters
- Non-obvious regex patterns must have explanatory comments - No trailing whitespace
### Parser Module API Convention ### Parser Module API
All parser modules (udt_parser, db_parser, fb_parser) must implement: All parser modules must implement:
- `parse_*_file(filepath)` - Parse a file and cache result - `parse_*_file(filepath)` - Parse and cache
- `parse_*_content(content, filename)` - Parse string content - `parse_*_content(content, filename)` - Parse string
- `get_*(name)` - Get single cached item by name - `get_*(name)` - Get cached item
- `get_all_*_names()` - Return list of all cached names - `get_all_*_names()` - List all cached names
- `get_*_members(name)` - Get members/fields for a type - `get_*_members(name)` - Get members/fields
- `is_*_type(name)` - Check if name is a known type - `is_*_type(name)` - Check if known type
- `clear_cache()` - Clear all cached data - `clear_cache()` - Clear cached data
- `get_cache_count()` - Return number of cached items - `get_cache_count()` - Return cache size
### Cache Management ### Cache Management
- Use module-level local tables for caching: `local cache = {}`
- Clear caches by iterating and setting to nil, not by reassigning:
```lua ```lua
local cache = {}
function M.clear_cache() function M.clear_cache()
for k in pairs(cache) do for k in pairs(cache) do
cache[k] = nil cache[k] = nil
@@ -106,21 +110,20 @@ end
## Lua Reserved Keywords ## Lua Reserved Keywords
Lua reserved keywords (like `end`, `for`, `in`, etc.) cannot be used as table keys directly. Use bracket notation: Cannot use reserved keywords as table keys directly:
```lua ```lua
-- WRONG: causes syntax error -- WRONG: syntax error
local range = { start = pos1, end = pos2 } local range = { start = pos1, end = pos2 }
-- CORRECT: use bracket notation -- CORRECT: bracket notation
local range = { start = pos1, ["end"] = pos2 } local range = { start = pos1, ["end"] = pos2 }
``` ```
This is especially important for LSP range objects which require an `end` field per the LSP specification. Critical for LSP range objects with `end` field.
## Error Handling ## Error Handling
### Return Pattern ### Return Pattern
Functions that can fail return `nil, "error message"`:
```lua ```lua
function M.parse_file(filepath) function M.parse_file(filepath)
local file = io.open(filepath, "r") local file = io.open(filepath, "r")
@@ -133,7 +136,6 @@ end
``` ```
### External Dependencies ### External Dependencies
Always wrap external requires in `pcall()`:
```lua ```lua
local ok, module = pcall(require, "some_module") local ok, module = pcall(require, "some_module")
if ok and module then if ok and module then
@@ -141,72 +143,187 @@ if ok and module then
end end
``` ```
### Validation ## Completion Triggers
Check required parameters early and return sensible defaults:
```lua
function M.get_members(type_name)
if not type_name then
return {}
end
-- ... continue
end
```
## Completion System
### Trigger Characters
- `#` - Local variables (after BEGIN) - `#` - Local variables (after BEGIN)
- `.` - Member access (UDT/DB members) - `.` - Member access (UDT/DB)
- `"` - Global DB names - `"` - Global DB names
- `(` - FB/Function parameters - `(` - FB/Function parameters
- ` ` (space) - General completion - ` ` (space) - General completion
### Active Portion Detection ## Formatter Options
When matching patterns in completion, use the "active portion" of the line (after the last operator) to correctly handle expressions like:
```scl The formatter supports collapsing verbose attribute blocks:
"DB1".member := "DB2".member
```lua
-- Before formatting:
statCntrPartsIn{EXTERNALACCESSIBLE := 'false'; EXTERNALVISIBLE := 'false'} : Int;
-- After formatting:
statCntrPartsIn{...} : Int;
``` ```
Find the last `:=`, `=>`, `=`, `<>`, `>=`, `<=`, `>`, `<`, `AND`, `OR`, `NOT` and only match patterns after it.
### Formatter Configuration
```lua
local options = {
insertSpaces = false,
tabSize = 1,
collapseAttributes = true, -- Enable attribute collapsing (default: true)
collapsePatterns = { -- Override default patterns
"^%s*{%s*EXTERNAL",
"^%s*{ S7_",
},
extendCollapsePatterns = { -- Add custom patterns
"^%s*{%s*CUSTOM",
},
}
```
### Default Collapse Patterns
- `^%s*{%s*EXTERNAL` - Variable attributes: `{EXTERNALACCESSIBLE := 'false'; ...}`
- `^%s*{ S7_` - Block attributes: `{ S7_Optimized_Access := 'TRUE' }`
- `^%s*{%s*%w+%s*:=` - Generic attributes with assignments
### Per-Variable Collapse Rules
Control collapsing per variable using rules (evaluated before global patterns):
```lua
local options = {
collapseVariableRules = {
-- Collapse attributes for variables starting with "stat"
{
variablePattern = "^stat",
collapse = true,
},
-- Never collapse for "temp" prefix variables
{
variablePattern = "^temp",
collapse = false,
},
-- Collapse only if both variable AND attribute match
{
variablePattern = "^config",
attributePattern = "EXTERNAL",
collapse = true,
},
},
}
```
**Rule properties:**
- `variablePattern` - Lua pattern to match variable name (optional)
- `attributePattern` - Lua pattern to match attribute content (optional)
- `collapse` - `true` to collapse to `{...}`, `false` to keep expanded
Rules are checked in order; first match wins.
## Commands ## Commands
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `:SCLShowVariables` | Show local variables in current file | | `:SCLShowVariables` | Show local variables |
| `:SCLShowWorkspaceTypes` | Show workspace UDTs, FBs, and Global DBs count | | `:SCLShowWorkspaceTypes` | Show workspace types count |
| `:SCLRescanWorkspaceTypes` | Rescan workspace for types and DBs | | `:SCLRescanWorkspaceTypes` | Rescan workspace |
| `:SCLPrefixWord` | Manually prefix current word with `#` | | `:SCLPrefixWord` | Prefix word with `#` |
| `:SCLMultilineParams` | Fill multiline parameters for FB/Function call | | `:SCLMultilineParams` | Fill FB parameters |
| `:LspSCLFormat` | Format current SCL file | | `:LspSCLFormat` | Format SCL file |
| `:SCLGeneratePlcJson` | Generate plc.data.json from data_types/ | | `:SCLToggleAttrBlock` | Toggle attribute block under cursor |
| `:SCLExpandAllAttrBlocks` | Expand all `{...}` blocks in buffer |
| `:SCLCollapseAllAttrBlocks` | Collapse all matching attribute blocks |
## LSP Server Implementation Notes ## Keybindings (SCL/UDT files)
### Attribute Block Toggle
Interactive expand/collapse of individual `{...}` blocks (uses `<Leader>x` prefix):
| Key | Mode | Description |
|-----|------|-------------|
| `<Leader>xa` | Normal/Insert | Toggle block under cursor |
| `<Leader>xae` | Normal | Expand all collapsed blocks |
| `<Leader>xac` | Normal | Collapse all attribute blocks |
**How it works:**
1. Place cursor inside any `{...}` block
2. Press `<Leader>xa` to toggle between collapsed `{...}` and expanded content
3. Original content is stored per-buffer and persists until buffer is closed
4. Works on both formatter-collapsed blocks and manually collapsed ones
### Other Keybindings
| Key | Mode | Description |
|-----|------|-------------|
| `<Leader>xa` | Normal/Insert | Toggle attribute block under cursor |
| `<Leader>xae` | Normal | Expand all attribute blocks |
| `<Leader>xac` | Normal | Collapse all attribute blocks |
| `<Space>mp` | Insert | Fill multiline FB parameters |
| `<Tab>` | Insert | Jump to next parameter or regular Tab |
Note: `<Leader>` is typically `\` (backslash) or `<Space>` depending on your configuration.
### Troubleshooting
**If commands or keybindings don't work:**
1. Check if setup() was called:
```lua
:lua print(vim.inspect(require("scl_lsp")))
```
2. Verify commands exist:
```vim
:command SCLToggleAttrBlock
```
Should show the command definition.
3. Check for errors during setup:
```lua
:lua require("scl_lsp").setup({})
```
4. Verify leader key:
```vim
:echo mapleader
```
If empty, your leader is `\` (backslash).
5. Manual test keybinding:
```vim
:nmap <Leader>xa
```
Should show the mapping.
## Testing
Manual testing using: `~/dev/siemens/projects/scl_lang_support_lazyvim_ref_project`
Workflow:
1. Open `.scl` file in Neovim
2. Test LSP features (hover, completion, go-to-definition)
3. Verify diagnostics
4. Test formatting
5. Check workspace scanning
## LSP Implementation Notes
### Method Name Conversion ### Method Name Conversion
LSP methods like `textDocument/didOpen` are converted to handler names like `textDocument_didOpen`:
```lua ```lua
local method_name = message.method:gsub("/", "_") local method_name = message.method:gsub("/", "_")
local handler = handlers[method_name] local handler = handlers[method_name]
``` ```
### Request vs Notification ### Request vs Notification
- Requests have `id` field and require a response
- Notifications have no `id` and should not receive a response
```lua ```lua
if message.id then if message.id then
-- Only send response for requests -- Request: send response
return { id = message.id, result = result } return { id = message.id, result = result }
end end
-- Notification: no response needed
``` ```
### Line Ending Handling ### Script Path Pattern
The LSP server strips CRLF (`\r\n`) line endings for Windows compatibility:
```lua ```lua
line = line:gsub("\r$", "") local script_path = debug.getinfo(1, "S").source:gsub("^@", ""):match("(.*/)") or ""
if script_path == "" then
script_path = "."
end
package.path = package.path .. ";" .. script_path .. "/?.lua"
``` ```
### JSON Parser
The standalone JSON parser in `src/json.lua` handles:
- Objects, arrays, strings, numbers, booleans, null
- Escape sequences in strings
- Whitespace skipping
+26 -10
View File
@@ -98,8 +98,8 @@ function M.validate_diagnostics(content, workspace_types, root_dir)
end end
end end
if in_var_section and trimmed:match("^[%w_]+%s*:") then if in_var_section and (trimmed:match("^[%w_]+%s*%b{}%s*:") or trimmed:match("^[%w_]+%s*:")) then
local var_name = trimmed:match("^([%w_]+)%s*:") local var_name = trimmed:match("^([%w_]+)%s*%b{}%s*:") or trimmed:match("^([%w_]+)%s*:")
local var_start = line:find(var_name, 1, true) local var_start = line:find(var_name, 1, true)
if var_name and not var_name:match("^%d") then if var_name and not var_name:match("^%d") then
@@ -114,21 +114,37 @@ function M.validate_diagnostics(content, workspace_types, root_dir)
}) })
end 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 if type_part then
type_part = type_part:gsub("%s*:=.*$", "") type_part = type_part:gsub("%s*:=.*$", "")
type_part = type_part:gsub("%s*$", "") 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 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 if base_type and not all_types[base_type] and
base_type ~= "BOOL" and base_type ~= "INT" and base_type ~= "DINT" and upper_type ~= "BOOL" and upper_type ~= "INT" and upper_type ~= "DINT" and
base_type ~= "REAL" and base_type ~= "LREAL" and base_type ~= "BYTE" and upper_type ~= "REAL" and upper_type ~= "LREAL" and upper_type ~= "BYTE" and
base_type ~= "WORD" and base_type ~= "DWORD" and base_type ~= "LWORD" and upper_type ~= "WORD" and upper_type ~= "DWORD" and upper_type ~= "LWORD" and
base_type ~= "CHAR" and base_type ~= "STRING" and base_type ~= "WCHAR" and upper_type ~= "CHAR" and upper_type ~= "STRING" and upper_type ~= "WCHAR" and
base_type ~= "WSTRING" and base_type ~= "TIME" and base_type ~= "DATE" and upper_type ~= "WSTRING" and upper_type ~= "TIME" and upper_type ~= "DATE" and
base_type ~= "TIME_OF_DAY" and base_type ~= "DATE_AND_TIME" and upper_type ~= "TIME_OF_DAY" and upper_type ~= "DATE_AND_TIME" and
base_type ~= "S5TIME" and base_type ~= "LTIME" then upper_type ~= "S5TIME" and upper_type ~= "LTIME" then
table.insert(diagnostics, { table.insert(diagnostics, {
range = range_to_lsp(line_num, 0, line_num, #line), range = range_to_lsp(line_num, 0, line_num, #line),
severity = DiagnosticSeverity.Warning, severity = DiagnosticSeverity.Warning,
+122 -14
View File
@@ -2,6 +2,17 @@
-- Stack-based implementation for proper nested structure handling -- Stack-based implementation for proper nested structure handling
local M = {} 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) local function position(line, character)
return { line = line, character = character } return { line = line, character = character }
end end
@@ -85,6 +96,21 @@ function M.format_document(content, options)
local use_spaces = options.insertSpaces or false 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" 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 -- Parse content into lines
local lines = {} local lines = {}
@@ -127,10 +153,73 @@ function M.format_document(content, options)
return base .. string.rep(" ", fb_paren_offset) return base .. string.rep(" ", fb_paren_offset)
end end
-- Helper: check if line is a case label -- 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*:%s*$") ~= nil return trimmed:match("^[%w_#]+%s*:%s*$") ~= nil
end 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 -- Helper: check if line starts FB call
local function is_fb_call_start(line) 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 trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
local kw = get_keyword(trimmed) local kw = get_keyword(trimmed)
-- Handle empty lines (preserve as-is) -- Handle empty lines (preserve as-is)
if trimmed == "" then if trimmed == "" then
table.insert(formatted_lines, "") table.insert(formatted_lines, "")
goto continue goto continue
end end
-- Handle comments (re-indent with current level) -- Handle comments (re-indent with current level)
if trimmed:match("^//") or trimmed:match("^%(") then if trimmed:match("^//") or trimmed:match("^%(") then
table.insert(formatted_lines, get_indent() .. trimmed) -- Apply attribute block collapsing to comments too (for consistency)
goto continue local collapsed = collapse_attribute_block(trimmed)
end table.insert(formatted_lines, get_indent() .. collapsed)
goto continue
end
-- Apply attribute block collapsing
trimmed = collapse_attribute_block(trimmed)
-- Check for FB call start -- Check for FB call start
if not in_fb_call and is_fb_call_start(trimmed) then if not in_fb_call and is_fb_call_start(trimmed) then
@@ -436,6 +530,20 @@ function M.get_formatting_options()
trimTrailingWhitespace = true, trimTrailingWhitespace = true,
insertFinalNewline = true, insertFinalNewline = true,
trimFinalNewlines = 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 end
+14 -3
View File
@@ -78,15 +78,26 @@ function M.extract_variables(content)
return return
end 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 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 end
if var_name and not variables[var_name] then if var_name and not variables[var_name] then
local col = line:find(var_name, 1, true) local col = line:find(var_name, 1, true)
if col then 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" local var_data_type = "unknown"
if type_start then if type_start then
local type_part = line:sub(type_start + 1) local type_part = line:sub(type_start + 1)